Merge branch 'github-main' into release/1.0.4

# Conflicts:
#	i18n/en_US.yaml
This commit is contained in:
LinkinStars 2023-02-07 17:38:58 +08:00
commit a8c2982a91
5 changed files with 333 additions and 106 deletions

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

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

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

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

View File

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

View File

@ -3,245 +3,247 @@
backend:
base:
success:
other: "Success."
other: Success.
unknown:
other: "Unknown error."
other: Unknown error.
request_format_error:
other: "Request format is not valid."
other: Request format is not valid.
unauthorized_error:
other: "Unauthorized."
other: Unauthorized.
database_error:
other: "Data server error."
other: Data server error.
role:
name:
user:
other: "User"
other: User
admin:
other: "Admin"
other: Admin
moderator:
other: "Moderator"
other: Moderator
description:
user:
other: "Default with no special access."
other: Default with no special access.
admin:
other: "Have the full power to access the site."
other: Have the full power to access the site.
moderator:
other: "Has access to all posts except admin settings."
other: Has access to all posts except admin settings.
email:
other: "Email"
other: Email
password:
other: "Password"
email_or_password_wrong_error: &email_or_password_wrong
other: "Email and password do not match."
other: Password
email_or_password_wrong_error:
other: Email and password do not match.
error:
admin:
email_or_password_wrong: *email_or_password_wrong
email_or_password_wrong:
other: Email and password do not match.
answer:
not_found:
other: "Answer do not found."
other: Answer do not found.
cannot_deleted:
other: "No permission to delete."
other: No permission to delete.
cannot_update:
other: "No permission to update."
other: No permission to update.
comment:
edit_without_permission:
other: "Comment are not allowed to edit."
other: Comment are not allowed to edit.
not_found:
other: "Comment not found."
other: Comment not found.
cannot_edit_after_deadline:
other: "The comment time has been too long to modify."
other: The comment time has been too long to modify.
email:
duplicate:
other: "Email already exists."
other: Email already exists.
need_to_be_verified:
other: "Email should be verified."
other: Email should be verified.
verify_url_expired:
other: "Email verified URL has expired, please resend the email."
other: Email verified URL has expired, please resend the email.
lang:
not_found:
other: "Language file not found."
other: Language file not found.
object:
captcha_verification_failed:
other: "Captcha wrong."
other: Captcha wrong.
disallow_follow:
other: "You are not allowed to follow."
other: You are not allowed to follow.
disallow_vote:
other: "You are not allowed to vote."
other: You are not allowed to vote.
disallow_vote_your_self:
other: "You can't vote for your own post."
other: You can't vote for your own post.
not_found:
other: "Object not found."
other: Object not found.
verification_failed:
other: "Verification failed."
other: Verification failed.
email_or_password_incorrect:
other: "Email and password do not match."
other: Email and password do not match.
old_password_verification_failed:
other: "The old password verification failed"
other: The old password verification failed
new_password_same_as_previous_setting:
other: "The new password is the same as the previous one."
other: The new password is the same as the previous one.
question:
not_found:
other: "Question not found."
other: Question not found.
cannot_deleted:
other: "No permission to delete."
other: No permission to delete.
cannot_close:
other: "No permission to close."
other: No permission to close.
cannot_update:
other: "No permission to update."
other: No permission to update.
rank:
fail_to_meet_the_condition:
other: "Rank fail to meet the condition."
other: Rank fail to meet the condition.
report:
handle_failed:
other: "Report handle failed."
other: Report handle failed.
not_found:
other: "Report not found."
other: Report not found.
tag:
not_found:
other: "Tag not found."
other: Tag not found.
recommend_tag_not_found:
other: "Recommend Tag is not exist."
other: Recommend Tag is not exist.
recommend_tag_enter:
other: "Please enter at least one required tag."
other: Please enter at least one required tag.
not_contain_synonym_tags:
other: "Should not contain synonym tags."
other: Should not contain synonym tags.
cannot_update:
other: "No permission to update."
other: No permission to update.
cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself."
other: You cannot set the synonym of the current tag as itself.
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
other: The From Name cannot be a email address.
theme:
not_found:
other: "Theme not found."
other: Theme not found.
revision:
review_underway:
other: "Can't edit currently, there is a version in the review queue."
other: Can't edit currently, there is a version in the review queue.
no_permission:
other: "No permission to Revision."
other: No permission to Revision.
user:
email_or_password_wrong:
other: *email_or_password_wrong
other:
other: Email and password do not match.
not_found:
other: "User not found."
other: User not found.
suspended:
other: "User has been suspended."
other: User has been suspended.
username_invalid:
other: "Username is invalid."
other: Username is invalid.
username_duplicate:
other: "Username is already in use."
other: Username is already in use.
set_avatar:
other: "Avatar set failed."
other: Avatar set failed.
cannot_update_your_role:
other: "You cannot modify your role."
other: You cannot modify your role.
not_allowed_registration:
other: "Currently the site is not open for registration"
other: Currently the site is not open for registration
config:
read_config_failed:
other: "Read config failed"
other: Read config failed
database:
connection_failed:
other: "Database connection failed"
other: Database connection failed
create_table_failed:
other: "Create table failed"
other: Create table failed
install:
create_config_failed:
other: "Can't create the config.yaml file."
other: Can't create the config.yaml file.
upload:
unsupported_file_format:
other: Unsupported file format.
report:
spam:
name:
other: "spam"
other: spam
desc:
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
other: This post is an advertisement, or vandalism. It is not useful or relevant
to the current topic.
rude:
name:
other: "rude or abusive"
other: rude or abusive
desc:
other: "A reasonable person would find this content inappropriate for respectful discourse."
other: A reasonable person would find this content inappropriate for respectful
discourse.
duplicate:
name:
other: "a duplicate"
other: a duplicate
desc:
other: "This question has been asked before and already has an answer."
other: This question has been asked before and already has an answer.
not_answer:
name:
other: "not an answer"
other: not an answer
desc:
other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether."
other: This was posted as an answer, but it does not attempt to answer the
question. It should possibly be an edit, a comment, another question,
or deleted altogether.
not_need:
name:
other: "no longer needed"
other: no longer needed
desc:
other: "This comment is outdated, conversational or not relevant to this post."
other: This comment is outdated, conversational or not relevant to this post.
other:
name:
other: "something else"
other: something else
desc:
other: "This post requires staff attention for another reason not listed above."
other: This post requires staff attention for another reason not listed above.
question:
close:
duplicate:
name:
other: "spam"
other: spam
desc:
other: "This question has been asked before and already has an answer."
other: This question has been asked before and already has an answer.
guideline:
name:
other: "a community-specific reason"
other: a community-specific reason
desc:
other: "This question doesn't meet a community guideline."
other: This question doesn't meet a community guideline.
multiple:
name:
other: "needs details or clarity"
other: needs details or clarity
desc:
other: "This question currently includes multiple questions in one. It should focus on one problem only."
other: This question currently includes multiple questions in one. It should
focus on one problem only.
other:
name:
other: "something else"
other: something else
desc:
other: "This post requires another reason not listed above."
other: This post requires another reason not listed above.
operation_type:
asked:
other: "asked"
other: asked
answered:
other: "answered"
other: answered
modified:
other: "modified"
other: modified
notification:
action:
update_question:
other: "updated question"
other: updated question
answer_the_question:
other: "answered question"
other: answered question
update_answer:
other: "updated answer"
other: updated answer
accept_answer:
other: "accepted answer"
other: accepted answer
comment_question:
other: "commented question"
other: commented question
comment_answer:
other: "commented answer"
other: commented answer
reply_to_you:
other: "replied to you"
other: replied to you
mention_you:
other: "mentioned you"
other: mentioned you
your_question_is_closed:
other: "Your question has been closed"
other: Your question has been closed
your_question_was_deleted:
other: "Your question has been deleted"
other: Your question has been deleted
your_answer_was_deleted:
other: "Your answer has been deleted"
other: Your answer has been deleted
your_comment_was_deleted:
other: "Your comment has been deleted"
other: Your comment has been deleted
# The following fields are used for interface presentation(Front-end)
ui:

View File

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