diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml new file mode 100644 index 00000000..8786952c --- /dev/null +++ b/.github/workflows/uffizzi-build.yml @@ -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 diff --git a/.github/workflows/uffizzi-preview.yml b/.github/workflows/uffizzi-preview.yml new file mode 100644 index 00000000..f7909741 --- /dev/null +++ b/.github/workflows/uffizzi-preview.yml @@ -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<> $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 diff --git a/docker-compose.uffizzi.yml b/docker-compose.uffizzi.yml new file mode 100644 index 00000000..f26d2bdc --- /dev/null +++ b/docker-compose.uffizzi.yml @@ -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: diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index b58a870f..e02c3382 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -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: diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index 3d903a14..bff57486 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -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