From 1271c346ae705530e7ecbc2a1f74c5d54d41331f Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+brawaru@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:32:46 +0100 Subject: [PATCH] Switch to manual Crowdin uploading and downloading Changes highlights ================== This commit adds two GitHub Actions workflow: i18n-pull --------- i18n-pull downloads translations from Crowdin. It is run automatically every Monday at 7 AM GMT time. It builds the Crowdin project and then downloads all the files corresponding to the branch it is running for (for scheduled runs it is the default branch of the repository). i18n-push --------- i18n-push runs the the extractor and uploads the source string files to Crowdin for translation. It is running for every push to the `master` branch and checks the changed files, trying to avoid unnecessary runs when none of the files changed contain translations. It can also be dispatched manually for every branch of the repository, which allows to create feature branches on Crowdin and give translators an early start. Translations for such branches can be downloaded by dispatching i18n-pull. They will also migrate automatically if the feature branch gets merged (see below). i18n-cleanup ------------ i18n-cleanup performs a cleanup of every repository branch deletion by deleting the translation branch on Crowdin as well Git pull branch. Deleting the translation branch after the merge allows Crowdin to migrate the translations to the other branch where updated strings now match (if no strings match, e.g., after PR rejection, then translations will just be deleted). Deletion of the pull branch closes the associated pull request which is most likely no longer relevant. *** Since translations are now extracted automatically, there is no more need in index.json file being a part of the repository, which previously caused pains when dealing with pull requests, so it is removed now. Contibutors will have to run `pnpm intl:extract` manually now. Migration strategy ================== Please take extra care when migrating to this approach to avoid the loss of translations. 1 - Creating a Crowdin token ---------------------------- The first step is to create a token in order for Crowdin CLI to perform operations. 1. Navigate to the settings for the automated account on Crowdin, and then switch to API tab. Alternatively, use the link: https://crowdin.com/settings#api-key. 2. Click on "New Token" 3. Give token a meaningful name to distinguish its requests in security logs 4. In scopes select the following: - Projects - Projects (List, Get, Create, Edit) - Source files & strings - Translations 5. Click "Create" 6. Confirm your identity by signing in again 7. Save the access token somewhere temporarily, you will need it later 2 - Creating a GitHub token --------------------------- In order for the download workflow to be able to create proper pull requests, it needs to puppeteer a user account. This is required for several reasons: - Other actions do not run on PRs created by actions - GitHub does not allow actions to create pull requests by default regardless the requested permissions in the workflow file. It requires enabling a dangerous permission in the organisation settings. - Bot's identity. Bot's cute. To create a token: 1. Log in to the GitHub account that will be automated 2. Go to Settings > Developer Settings > Personal access tokens > Tokens (classic) 3. Click "Generate new token" and select "Generate new token (classic)" from the menu 4. Give token a meaningful name and reasonable expiration time 5. In scopes select `repo` scope 6. Save the token 3 - Setting up GitHub Actions secrets and variables -------------------------------------------------- For workflows to work correctly, they need several variables. Assuming you completed the above steps, here's how you set them up. 1. Navigate to the modrinth/knossos repository 2. Switch to Settings tab 3. Go to Secrets and variables > Actions 4. Create the following repository secrets: - `CROWDIN_PERSONAL_TOKEN`: [your Crowdin token] - `CROWDIN_GH_TOKEN`: [your GitHub automated account token] 5. Create the following repository variables: - `CROWDIN_PROJECT_ID`: [Crowdin project ID (Modrinth - 518556)] 4 - Preparing Crowdin project ----------------------------- Because workflows will replace GitHub integration, that integration needs to be disabled before the merge to avoid conflics and accidental deletion of the translations due to 'deletion' of the source files. 1. Go to Modrinth Crowdin project 2. Switch to Integrations tab 3. Click on GitHub integration 4. Click on modrinth/knossos repository 5. Click "Delete integration" 6. Confirm deletion 5 - Merge --------- After following all the steps above you should be now ready to merge this pull request. Make sure to check that actions do run after you merge it. --- .github/workflows/i18n-cleanup.yml | 103 ++++ .github/workflows/i18n-pull.yml | 123 ++++ .github/workflows/i18n-push.yml | 149 +++++ .gitignore | 3 + crowdin.yml | 7 +- locales/en-US/index.json | 911 ----------------------------- package.json | 2 +- 7 files changed, 384 insertions(+), 914 deletions(-) create mode 100644 .github/workflows/i18n-cleanup.yml create mode 100644 .github/workflows/i18n-pull.yml create mode 100644 .github/workflows/i18n-push.yml delete mode 100644 locales/en-US/index.json diff --git a/.github/workflows/i18n-cleanup.yml b/.github/workflows/i18n-cleanup.yml new file mode 100644 index 0000000000..8c226068e3 --- /dev/null +++ b/.github/workflows/i18n-cleanup.yml @@ -0,0 +1,103 @@ +name: Crowdin (cleanup) + +on: [delete] + +concurrency: + group: i18n-management + +env: + CROWDIN_CLI_VERSION: '3.18.0' + +jobs: + preflight_check: + name: 'Pre-flight check' + runs-on: ubuntu-22.04 + concurrency: + group: i18n-cleanup:${{ github.event.ref }} + cancel-in-progress: true + if: github.event.ref_type == 'branch' && !startsWith(github.event.ref, 'ref/heads/crowdin-pull/') + steps: + - name: Preflight check + id: check + shell: bash + run: | + PREFLIGHT_CHECK_RESULT=true + + function flight_failure () { + if [ "$PREFLIGHT_CHECK_RESULT" = true ]; then + echo "One or more pre-flight checks failed!" + echo "" + PREFLIGHT_CHECK_RESULT=false + fi + echo "- $1" + } + + if [ "$CROWDIN_PROJECT_ID_DEFINED" != true ]; then + flight_failure "CROWDIN_PROJECT_ID variable is not defined (required to push)" + fi + + if [ "$CROWDIN_PERSONAL_TOKEN_DEFINED" != true ]; then + flight_failure "CROWDIN_PERSONAL_TOKEN secret is not defined (required to push)" + fi + + echo "flight_ok=$PREFLIGHT_CHECK_RESULT" >> "$GITHUB_OUTPUT" + env: + CROWDIN_PROJECT_ID_DEFINED: ${{ vars.CROWDIN_PROJECT_ID != '' }} + CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }} + outputs: + ready: ${{ steps.check.outputs.flight_ok == 'true' }} + delete_crowdin_branch: + name: 'Delete Crowdin branch' + runs-on: ubuntu-22.04 + needs: preflight_check + if: needs.preflight_check.outputs.ready == 'true' + concurrency: + group: i18n-cleanup:${{ github.event.ref }} + cancel-in-progress: true + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get branch name from ref + id: branch-name + shell: bash + run: echo "safe_branch_name=$(echo "${{ github.event.ref }}" | sed -e "s/[\\\\/\\:*?\"<>|]/_/g")" >> "$GITHUB_OUTPUT" + + # Crowdin GitHub Action is currently broken and doesn't properly run commands that contain spaces + # See https://github.com/crowdin/github-action/issues/192 + - name: Download Crowdin CLI + uses: robinraju/release-downloader@v1.9 + with: + repository: crowdin/crowdin-cli + tag: ${{ env.CROWDIN_CLI_VERSION }} + fileName: 'crowdin-cli.zip' + out-file-path: '.crowdin' + + - name: Delete translations branch on Crowdin + shell: bash + run: | + CROWDIN_DIR="$PWD/.crowdin" + unzip "$CROWDIN_DIR/crowdin-cli.zip" -d "$CROWDIN_DIR" + CROWDIN_CLI_JAR="$CROWDIN_DIR/${{ env.CROWDIN_CLI_VERSION }}/crowdin-cli.jar" + java -jar "$CROWDIN_CLI_JAR" branch delete "[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}" + continue-on-error: true + env: + CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + + # - name: Delete translations branch on Crowdin + # uses: crowdin/github-action@v1 + # with: + # command: 'branch' + # command_args: 'delete "[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}"' + # env: + # CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + # CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + # continue-on-error: true + + - name: Delete Git branch + shell: bash + run: 'git push origin :crowdin-pull/${{ github.event.ref }}' + continue-on-error: true diff --git a/.github/workflows/i18n-pull.yml b/.github/workflows/i18n-pull.yml new file mode 100644 index 0000000000..f9c9e9556f --- /dev/null +++ b/.github/workflows/i18n-pull.yml @@ -0,0 +1,123 @@ +name: Crowdin (pull) + +on: + schedule: + - cron: '0 7 * * MON' # every monday at 7 am + workflow_dispatch: {} + +concurrency: + group: i18n-management + +jobs: + preflight_check: + name: 'Pre-flight check' + runs-on: ubuntu-22.04 + concurrency: + group: i18n-pull:${{ github.ref }} + cancel-in-progress: true + steps: + - name: Preflight check + id: check + run: | + PREFLIGHT_CHECK_RESULT=true + + function flight_failure () { + if [ "$PREFLIGHT_CHECK_RESULT" = true ]; then + echo "One or more pre-flight checks failed!" + echo "" + PREFLIGHT_CHECK_RESULT=false + fi + echo "- $1" + } + + if [ "$CROWDIN_PROJECT_ID_DEFINED" != true ]; then + flight_failure "CROWDIN_PROJECT_ID variable is not defined (required to push)" + fi + + if [ "$CROWDIN_PERSONAL_TOKEN_DEFINED" != true ]; then + flight_failure "CROWDIN_PERSONAL_TOKEN secret is not defined (required to push)" + fi + + if [ "$CROWDIN_GH_TOKEN_DEFINED" != true ]; then + flight_failure "CROWDIN_GH_TOKEN secret is not defined (required to make pull requests)" + fi + + echo "flight_ok=$PREFLIGHT_CHECK_RESULT" >> "$GITHUB_OUTPUT" + env: + CROWDIN_PROJECT_ID_DEFINED: ${{ vars.CROWDIN_PROJECT_ID != '' }} + CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }} + CROWDIN_GH_TOKEN_DEFINED: ${{ secrets.CROWDIN_GH_TOKEN != '' }} + outputs: + ready: ${{ steps.check.outputs.flight_ok == 'true' }} + + pull_translations: + name: 'Pull translations from Crowdin' + needs: preflight_check + if: needs.preflight_check.outputs.ready == 'true' + runs-on: ubuntu-22.04 + concurrency: + group: i18n-pull:${{ github.ref }} + cancel-in-progress: true + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + token: ${{ secrets.CROWDIN_GH_TOKEN }} + + - name: Configure Git author + id: git-author + uses: MarcoIeni/git-config@v0.1 + env: + GITHUB_TOKEN: ${{ secrets.CROWDIN_GH_TOKEN }} + + # Because --all flag of Crowdin CLI is currently broken we need to create a fake source file + # so that the CLI won't omit translations for it. See https://github.com/crowdin/crowdin-cli/issues/724 + - name: Write fake sources + shell: bash + run: echo "{}" > locales/en-US/index.json + + - name: Query branch name + id: branch-name + shell: bash + run: | + BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed -e "s/[\\\\/\\:*?\"<>|]/_/g") + echo "Branch name is $BRANCH_NAME (escaped as $SAFE_BRANCH_NAME)" + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT" + + - name: Download translations from Crowdin + uses: crowdin/github-action@v1 + with: + upload_sources: false + upload_translations: false + download_translations: true + push_translations: false + create_pull_request: false + crowdin_branch_name: '[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}' + env: + CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + + - name: Fix broken permissions + shell: bash + run: sudo chown -R $USER:$USER locales + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + title: 'New translations from Crowdin (${{ steps.branch-name.outputs.branch_name }})' + body: |- + Here is your automated pull request with updated translations from [our Crowdin](https://crowdin.com/project/modrinth) for the `${{ steps.branch-name.outputs.branch_name }}` branch. Merge whenever you ready. + + This pull request is created according to the `.github/workflows/i18n-pull.yml` file. + + Want to update this pull request? [Dispatch this workflow again](https://github.com/${{ github.repository }}/actions/workflows/i18n-pull.yml). + commit-message: 'New translations from Crowdin (${{ steps.branch-name.outputs.branch_name }})' + branch: crowdin-pull/${{ steps.branch-name.outputs.branch_name }} + add-paths: locales + author: '${{ steps.git-author.outputs.name }} <${{ steps.git-author.outputs.email }}>' + committer: '${{ steps.git-author.outputs.name }} <${{ steps.git-author.outputs.email }}>' + token: ${{ secrets.CROWDIN_GH_TOKEN }} + git-token: ${{ secrets.CROWDIN_GH_TOKEN }} diff --git a/.github/workflows/i18n-push.yml b/.github/workflows/i18n-push.yml new file mode 100644 index 0000000000..714e6bc705 --- /dev/null +++ b/.github/workflows/i18n-push.yml @@ -0,0 +1,149 @@ +name: Crowdin (push) + +on: + push: {} + workflow_dispatch: {} + +concurrency: + group: i18n-management + +jobs: + preflight_check: + name: 'Pre-flight check' + runs-on: ubuntu-22.04 + concurrency: + group: i18n-push:${{ github.ref }} + cancel-in-progress: true + steps: + - name: Preflight check + id: check + run: | + PREFLIGHT_CHECK_RESULT=true + + function flight_failure () { + if [ "$PREFLIGHT_CHECK_RESULT" = true ]; then + echo "One or more pre-flight checks failed!" + echo "" + PREFLIGHT_CHECK_RESULT=false + fi + echo "- $1" + } + + if [ "$CROWDIN_PROJECT_ID_DEFINED" != true ]; then + flight_failure "CROWDIN_PROJECT_ID variable is not defined (required to push)" + fi + + if [ "$CROWDIN_PERSONAL_TOKEN_DEFINED" != true ]; then + flight_failure "CROWDIN_PERSONAL_TOKEN secret is not defined (required to push)" + fi + + echo "flight_ok=$PREFLIGHT_CHECK_RESULT" >> "$GITHUB_OUTPUT" + env: + CROWDIN_PROJECT_ID_DEFINED: ${{ vars.CROWDIN_PROJECT_ID != '' }} + CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }} + + - name: Check that Crowdin branch exists + if: github.event_name != 'workflow_dispatch' && github.ref_name != github.event.repository.default_branch + id: crowdin-branch-exists + uses: GuillaumeFalourd/branch-exists@v1 + with: + branch: ${{ format('crowdin-pull/{0}', github.ref_name) }} + + - name: Checkout + id: checkout + if: github.event_name != 'workflow_dispatch' && steps.check.outputs.flight_ok == 'true' && (steps.crowdin-branch-exists.outcome == 'skipped' || steps.crowdin-branch-exists.outputs.exists == 'true') + uses: actions/checkout@v4 + + - name: Confirm push necessity + id: changed-files + if: steps.checkout.outcome != 'skipped' + uses: tj-actions/changed-files@v42 + with: + negation_patterns_first: true + files: | + ** + locales/en-US/** + crowdin.yml + files_ignore: | + .{github,vscode,idea}/** + {assets,patches,types,public,locales}/** + .{editorconfig,gitignore,npmrc,prettierignore} + .*.{js,json} + *.{md,yml,yaml,json} + LICENSE + + - name: Output result + if: steps.changed-files.outcome != 'skipped' + run: | + cat << EOF + Changed files are ${{ steps.changed-files.outputs.all_changed_files }} + EOF + outputs: + ready: ${{ steps.check.outputs.flight_ok == 'true' && (github.event_name == 'workflow_dispatch' || steps.changed-files.outputs.any_changed == 'true') }} + + push_translations: + name: Push sources to Crowdin + needs: preflight_check + if: needs.preflight_check.outputs.ready == 'true' + concurrency: + group: i18n-push:${{ github.ref }} + cancel-in-progress: true + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 18.x + + - name: Install pnpm via corepack + shell: bash + run: | + corepack enable + corepack prepare --activate + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Extract translations + run: pnpm intl:extract + + - name: Query branch name + id: branch-name + shell: bash + run: | + BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed -e "s/[\\\\/\\:*?\"<>|]/_/g") + echo "Branch name is $BRANCH_NAME (escaped as $SAFE_BRANCH_NAME)" + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT" + + - name: Upload translations to Crowdin + uses: crowdin/github-action@v1 + with: + upload_sources: true + upload_translations: false + download_translations: false + push_translations: false + create_pull_request: false + crowdin_branch_name: '[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}' + env: + CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.gitignore b/.gitignore index 9678d9bd0b..12313ac85a 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,6 @@ sw.* # Vim swap files *.swp + +# Automatically generated locale files +/locales/en-US/index.json diff --git a/crowdin.yml b/crowdin.yml index 2817cde427..66fda704b6 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,8 +1,11 @@ -project_id: 518556 preserve_hierarchy: true commit_message: '[ci skip]' files: - - source: /locales/en-US/* + - source: /locales/en-US/*.json dest: /%original_file_name% translation: /locales/%locale%/%original_file_name% + skip_untranslated_strings: true + +project_id_env: CROWDIN_PROJECT_ID +api_token_env: CROWDIN_PERSONAL_TOKEN diff --git a/locales/en-US/index.json b/locales/en-US/index.json deleted file mode 100644 index 2459075245..0000000000 --- a/locales/en-US/index.json +++ /dev/null @@ -1,911 +0,0 @@ -{ - "auth.authorize.action.authorize": { - "message": "Authorize" - }, - "auth.authorize.action.decline": { - "message": "Decline" - }, - "auth.authorize.app-info": { - "message": "{appName} by {creator} will be able to:" - }, - "auth.authorize.authorize-app-name": { - "message": "Authorize {appName}" - }, - "auth.authorize.error.no-redirect-url": { - "message": "No redirect location found in response" - }, - "auth.authorize.redirect-url": { - "message": "You will be redirected to {url}" - }, - "auth.reset-password.method-choice.action": { - "message": "Send recovery email" - }, - "auth.reset-password.method-choice.description": { - "message": "Enter your email below and we'll send a recovery link to allow you to recover your account." - }, - "auth.reset-password.method-choice.email-username.label": { - "message": "Email or username" - }, - "auth.reset-password.method-choice.email-username.placeholder": { - "message": "Email" - }, - "auth.reset-password.notification.email-sent.text": { - "message": "An email with instructions has been sent to you if the email was previously saved on your account." - }, - "auth.reset-password.notification.email-sent.title": { - "message": "Email sent" - }, - "auth.reset-password.notification.password-reset.text": { - "message": "You can now log-in into your account with your new password." - }, - "auth.reset-password.notification.password-reset.title": { - "message": "Password successfully reset" - }, - "auth.reset-password.post-challenge.action": { - "message": "Reset password" - }, - "auth.reset-password.post-challenge.confirm-password.label": { - "message": "Confirm password" - }, - "auth.reset-password.post-challenge.description": { - "message": "Enter your new password below to gain access to your account." - }, - "auth.reset-password.title": { - "message": "Reset Password" - }, - "auth.reset-password.title.long": { - "message": "Reset your password" - }, - "auth.sign-in.2fa.description": { - "message": "Please enter a two-factor code to proceed." - }, - "auth.sign-in.2fa.label": { - "message": "Enter two-factor code" - }, - "auth.sign-in.2fa.placeholder": { - "message": "Enter code..." - }, - "auth.sign-in.additional-options": { - "message": "Forgot password?Create an account" - }, - "auth.sign-in.email-username.label": { - "message": "Email or username" - }, - "auth.sign-in.password.label": { - "message": "Password" - }, - "auth.sign-in.sign-in-with": { - "message": "Sign in with" - }, - "auth.sign-in.title": { - "message": "Sign In" - }, - "auth.sign-in.use-password": { - "message": "Or use a password" - }, - "auth.sign-up.action.create-account": { - "message": "Create account" - }, - "auth.sign-up.confirm-password.label": { - "message": "Confirm password" - }, - "auth.sign-up.email.label": { - "message": "Email" - }, - "auth.sign-up.label.username": { - "message": "Username" - }, - "auth.sign-up.legal-dislaimer": { - "message": "By creating an account, you agree to Modrinth's Terms and Privacy Policy." - }, - "auth.sign-up.notification.password-mismatch.text": { - "message": "Passwords do not match!" - }, - "auth.sign-up.password.label": { - "message": "Password" - }, - "auth.sign-up.sign-in-option.title": { - "message": "Already have an account?" - }, - "auth.sign-up.subscribe.label": { - "message": "Subscribe to updates about Modrinth" - }, - "auth.sign-up.title": { - "message": "Sign Up" - }, - "auth.sign-up.title.create-account": { - "message": "Or create an account yourself" - }, - "auth.sign-up.title.sign-up-with": { - "message": "Sign up with" - }, - "auth.verify-email.action.account-settings": { - "message": "Account settings" - }, - "auth.verify-email.action.sign-in": { - "message": "Sign in" - }, - "auth.verify-email.already-verified.description": { - "message": "Your email is already verified!" - }, - "auth.verify-email.already-verified.title": { - "message": "Email already verified" - }, - "auth.verify-email.failed-verification.action": { - "message": "Resend verification email" - }, - "auth.verify-email.failed-verification.description": { - "message": "We were unable to verify your email. Try re-sending the verification email through your dashboard by signing in." - }, - "auth.verify-email.failed-verification.description.logged-in": { - "message": "We were unable to verify your email. Try re-sending the verification email through the button below." - }, - "auth.verify-email.failed-verification.title": { - "message": "Email verification failed" - }, - "auth.verify-email.post-verification.description": { - "message": "Your email address has been successfully verified!" - }, - "auth.verify-email.post-verification.title": { - "message": "Email verification" - }, - "auth.verify-email.title": { - "message": "Verify Email" - }, - "auth.welcome.checkbox.subscribe": { - "message": "Subscribe to updates about Modrinth" - }, - "auth.welcome.description": { - "message": "Thank you for creating an account. You can now follow and create projects, receive updates about your favorite projects, and more!" - }, - "auth.welcome.label.tos": { - "message": "By creating an account, you have agreed to Modrinth's Terms and Privacy Policy." - }, - "auth.welcome.long-title": { - "message": "Welcome to Modrinth!" - }, - "auth.welcome.title": { - "message": "Welcome" - }, - "button.cancel": { - "message": "Cancel" - }, - "button.continue": { - "message": "Continue" - }, - "button.create-a-project": { - "message": "Create a project" - }, - "button.edit": { - "message": "Edit" - }, - "button.save": { - "message": "Save" - }, - "button.save-changes": { - "message": "Save changes" - }, - "button.sign-in": { - "message": "Sign in" - }, - "button.sign-out": { - "message": "Sign out" - }, - "collection.button.delete-icon": { - "message": "Delete icon" - }, - "collection.button.edit-icon": { - "message": "Edit icon" - }, - "collection.button.remove-project": { - "message": "Remove project" - }, - "collection.button.unfollow-project": { - "message": "Unfollow project" - }, - "collection.button.upload-icon": { - "message": "Upload icon" - }, - "collection.delete-modal.description": { - "message": "This will remove this collection forever. This action cannot be undone." - }, - "collection.delete-modal.title": { - "message": "Are you sure you want to delete this collection?" - }, - "collection.description": { - "message": "{description} - View the collection {name} by {username} on Modrinth" - }, - "collection.description.following": { - "message": "Auto-generated collection of all the projects you're following." - }, - "collection.error.not-found": { - "message": "Collection not found" - }, - "collection.label.collection": { - "message": "Collection" - }, - "collection.label.created-at": { - "message": "Created {ago}" - }, - "collection.label.curated-by": { - "message": "Curated by" - }, - "collection.label.no-projects": { - "message": "This collection has no projects!" - }, - "collection.label.no-projects-auth": { - "message": "You don't have any projects.\nWould you like to add one?" - }, - "collection.label.owner": { - "message": "Owner" - }, - "collection.label.private": { - "message": "Private" - }, - "collection.label.projects-count": { - "message": "{count, plural, one {{count} project} other {{count} projects}}" - }, - "collection.label.updated-at": { - "message": "Updated {ago}" - }, - "collection.title": { - "message": "{name} - Collection" - }, - "dashboard.collections.button.create-new": { - "message": "Create new" - }, - "dashboard.collections.label.projects-count": { - "message": "{count, plural, one {{count} project} other {{count} projects}}" - }, - "dashboard.collections.label.search-input": { - "message": "Search your collections" - }, - "dashboard.collections.long-title": { - "message": "Your collections" - }, - "frog": { - "message": "You've been frogged! 🐸" - }, - "frog.altText": { - "message": "A photorealistic painting of a frog labyrinth" - }, - "frog.froggedPeople": { - "message": "{count, plural, one {{count} more person} other {{count} more people}} were also frogged!" - }, - "frog.sinceOpened": { - "message": "This page was opened {ago}" - }, - "frog.title": { - "message": "Frog" - }, - "input.view.gallery": { - "message": "Gallery view" - }, - "input.view.grid": { - "message": "Grid view" - }, - "input.view.list": { - "message": "List view" - }, - "label.collections": { - "message": "Collections" - }, - "label.created-ago": { - "message": "Created {ago}" - }, - "label.dashboard": { - "message": "Dashboard" - }, - "label.delete": { - "message": "Delete" - }, - "label.description": { - "message": "Description" - }, - "label.error": { - "message": "Error" - }, - "label.followed-projects": { - "message": "Followed projects" - }, - "label.moderation": { - "message": "Moderation" - }, - "label.notifications": { - "message": "Notifications" - }, - "label.password": { - "message": "Password" - }, - "label.public": { - "message": "Public" - }, - "label.rejected": { - "message": "Rejected" - }, - "label.scopes": { - "message": "Scopes" - }, - "label.settings": { - "message": "Settings" - }, - "label.title": { - "message": "Title" - }, - "label.unlisted": { - "message": "Unlisted" - }, - "label.visibility": { - "message": "Visibility" - }, - "layout.action.change-theme": { - "message": "Change theme" - }, - "layout.action.get-modrinth-app": { - "message": "Get Modrinth App" - }, - "layout.avatar.alt": { - "message": "Your avatar" - }, - "layout.banner.add-email.button": { - "message": "Visit account settings" - }, - "layout.banner.add-email.title": { - "message": "For security purposes, please enter your email on Modrinth." - }, - "layout.banner.staging.description": { - "message": "The staging environment is running on a copy of the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance." - }, - "layout.banner.staging.title": { - "message": "You’re viewing Modrinth’s staging environment." - }, - "layout.banner.verify-email.action": { - "message": "Re-send verification email" - }, - "layout.banner.verify-email.title": { - "message": "For security purposes, please verify your email address on Modrinth." - }, - "layout.footer.company.careers": { - "message": "Careers" - }, - "layout.footer.company.privacy": { - "message": "Privacy" - }, - "layout.footer.company.rules": { - "message": "Rules" - }, - "layout.footer.company.terms": { - "message": "Terms" - }, - "layout.footer.company.title": { - "message": "Company" - }, - "layout.footer.interact.title": { - "message": "Interact" - }, - "layout.footer.legal-disclaimer": { - "message": "NOT AN OFFICIAL MINECRAFT SERVICE. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT." - }, - "layout.footer.open-source": { - "message": "Modrinth is open source." - }, - "layout.footer.resources.blog": { - "message": "Blog" - }, - "layout.footer.resources.docs": { - "message": "Docs" - }, - "layout.footer.resources.status": { - "message": "Status" - }, - "layout.footer.resources.support": { - "message": "Support" - }, - "layout.footer.resources.title": { - "message": "Resources" - }, - "layout.label.visit-your-profile": { - "message": "Visit your profile" - }, - "layout.menu-toggle.action": { - "message": "Toggle menu" - }, - "layout.meta.description": { - "message": "Download Minecraft mods, plugins, datapacks, shaders, resourcepacks, and modpacks on Modrinth. Discover and publish projects on Modrinth with a modern, easy to use interface and API." - }, - "layout.meta.og-description": { - "message": "Discover and publish Minecraft content!" - }, - "layout.nav.home": { - "message": "Home" - }, - "layout.nav.search": { - "message": "Search" - }, - "notification.error.title": { - "message": "An error occurred" - }, - "profile.button.manage-projects": { - "message": "Manage projects" - }, - "profile.button.report": { - "message": "Report" - }, - "profile.error.not-found": { - "message": "User not found" - }, - "profile.input.upload-avatar": { - "message": "Upload avatar" - }, - "profile.joined-at": { - "message": "Joined {ago}" - }, - "profile.label.edit-bio": { - "message": "Bio" - }, - "profile.label.edit-username": { - "message": "Username" - }, - "profile.label.no-collections": { - "message": "This user has no collections!" - }, - "profile.label.no-collections-auth": { - "message": "You don't have any collections.\nWould you like to create one?" - }, - "profile.label.no-projects": { - "message": "This user has no projects!" - }, - "profile.label.no-projects-auth": { - "message": "You don't have any projects.\nWould you like to create one?" - }, - "profile.label.organizations": { - "message": "Organizations" - }, - "profile.meta.description": { - "message": "Download {username}'s projects on Modrinth" - }, - "profile.meta.description-with-bio": { - "message": "{bio} - Download {username}'s projects on Modrinth" - }, - "profile.stats.downloads": { - "message": "{count, plural, one {{count} download} other {{count} downloads}}" - }, - "profile.stats.projects-followers": { - "message": "{count, plural, one {{count} follower} other {{count} followers}} of projects" - }, - "profile.user-id": { - "message": "User ID: {id}" - }, - "project-type.all": { - "message": "All" - }, - "project-type.collection.plural": { - "message": "Collections" - }, - "project-type.collection.singular": { - "message": "Collection" - }, - "project-type.datapack.plural": { - "message": "Data Packs" - }, - "project-type.datapack.singular": { - "message": "Data Pack" - }, - "project-type.mod.plural": { - "message": "Mods" - }, - "project-type.mod.singular": { - "message": "Mod" - }, - "project-type.modpack.plural": { - "message": "Modpacks" - }, - "project-type.modpack.singular": { - "message": "Modpack" - }, - "project-type.plugin.plural": { - "message": "Plugins" - }, - "project-type.plugin.singular": { - "message": "Plugin" - }, - "project-type.project.plural": { - "message": "Projects" - }, - "project-type.project.singular": { - "message": "Project" - }, - "project-type.resourcepack.plural": { - "message": "Resource Packs" - }, - "project-type.resourcepack.singular": { - "message": "Resource Pack" - }, - "project-type.shader.plural": { - "message": "Shaders" - }, - "project-type.shader.singular": { - "message": "Shader" - }, - "revenue.transfers.total": { - "message": "You have withdrawn {amount} in total." - }, - "revenue.transfers.total.method": { - "message": "You have withdrawn {amount} through {method}." - }, - "revenue.transfers.total.year": { - "message": "You have withdrawn {amount} in {year}." - }, - "revenue.transfers.total.year_method": { - "message": "You have withdrawn {amount} in {year} through {method}." - }, - "scopes.analytics.description": { - "message": "Access your analytics data" - }, - "scopes.analytics.label": { - "message": "Read analytics" - }, - "scopes.collectionCreate.description": { - "message": "Create collections" - }, - "scopes.collectionCreate.label": { - "message": "Create collections" - }, - "scopes.collectionDelete.description": { - "message": "Delete collections" - }, - "scopes.collectionDelete.label": { - "message": "Delete collections" - }, - "scopes.collectionRead.description": { - "message": "Read collections" - }, - "scopes.collectionRead.label": { - "message": "Read collections" - }, - "scopes.collectionWrite.description": { - "message": "Write to collections" - }, - "scopes.collectionWrite.label": { - "message": "Write collections" - }, - "scopes.notificationRead.description": { - "message": "Read your notifications" - }, - "scopes.notificationRead.label": { - "message": "Read notifications" - }, - "scopes.notificationWrite.description": { - "message": "Delete/View your notifications" - }, - "scopes.notificationWrite.label": { - "message": "Write notifications" - }, - "scopes.organizationCreate.description": { - "message": "Create organizations" - }, - "scopes.organizationCreate.label": { - "message": "Create organizations" - }, - "scopes.organizationDelete.description": { - "message": "Delete organizations" - }, - "scopes.organizationDelete.label": { - "message": "Delete organizations" - }, - "scopes.organizationRead.description": { - "message": "Read organizations" - }, - "scopes.organizationRead.label": { - "message": "Read organizations" - }, - "scopes.organizationWrite.description": { - "message": "Write to organizations" - }, - "scopes.organizationWrite.label": { - "message": "Write organizations" - }, - "scopes.patCreate.description": { - "message": "Create personal API tokens" - }, - "scopes.patCreate.label": { - "message": "Create PATs" - }, - "scopes.patDelete.description": { - "message": "Delete your personal API tokens" - }, - "scopes.patDelete.label": { - "message": "Delete PATs" - }, - "scopes.patRead.description": { - "message": "View created API tokens" - }, - "scopes.patRead.label": { - "message": "Read PATs" - }, - "scopes.patWrite.description": { - "message": "Edit personal API tokens" - }, - "scopes.patWrite.label": { - "message": "Write PATs" - }, - "scopes.payoutsRead.description": { - "message": "Read your payouts data" - }, - "scopes.payoutsRead.label": { - "message": "Read payouts" - }, - "scopes.payoutsWrite.description": { - "message": "Withdraw money" - }, - "scopes.payoutsWrite.label": { - "message": "Write payouts" - }, - "scopes.performAnalytics.description": { - "message": "Perform analytics actions" - }, - "scopes.performAnalytics.label": { - "message": "Perform analytics" - }, - "scopes.projectCreate.description": { - "message": "Create new projects" - }, - "scopes.projectCreate.label": { - "message": "Create projects" - }, - "scopes.projectDelete.description": { - "message": "Delete your projects" - }, - "scopes.projectDelete.label": { - "message": "Delete projects" - }, - "scopes.projectRead.description": { - "message": "Read all your projects" - }, - "scopes.projectRead.label": { - "message": "Read projects" - }, - "scopes.projectWrite.description": { - "message": "Write to project data" - }, - "scopes.projectWrite.label": { - "message": "Write projects" - }, - "scopes.reportCreate.description": { - "message": "Create reports" - }, - "scopes.reportCreate.label": { - "message": "Create reports" - }, - "scopes.reportDelete.description": { - "message": "Delete reports" - }, - "scopes.reportDelete.label": { - "message": "Delete reports" - }, - "scopes.reportRead.description": { - "message": "Read reports" - }, - "scopes.reportRead.label": { - "message": "Read reports" - }, - "scopes.reportWrite.description": { - "message": "Edit reports" - }, - "scopes.reportWrite.label": { - "message": "Write reports" - }, - "scopes.sessionAccess.description": { - "message": "Access modrinth-issued sessions" - }, - "scopes.sessionAccess.label": { - "message": "Access sessions" - }, - "scopes.sessionDelete.description": { - "message": "Delete sessions" - }, - "scopes.sessionDelete.label": { - "message": "Delete sessions" - }, - "scopes.sessionRead.description": { - "message": "Read active sessions" - }, - "scopes.sessionRead.label": { - "message": "Read sessions" - }, - "scopes.threadRead.description": { - "message": "Read threads" - }, - "scopes.threadRead.label": { - "message": "Read threads" - }, - "scopes.threadWrite.description": { - "message": "Write to threads" - }, - "scopes.threadWrite.label": { - "message": "Write threads" - }, - "scopes.userAuthWrite.description": { - "message": "Modify your authentication data" - }, - "scopes.userAuthWrite.label": { - "message": "Write auth data" - }, - "scopes.userDelete.description": { - "message": "Delete your account" - }, - "scopes.userDelete.label": { - "message": "Delete your account" - }, - "scopes.userRead.description": { - "message": "Access your public profile information" - }, - "scopes.userRead.label": { - "message": "Read user data" - }, - "scopes.userReadEmail.description": { - "message": "Read your email" - }, - "scopes.userReadEmail.label": { - "message": "Read user email" - }, - "scopes.userWrite.description": { - "message": "Write to your profile" - }, - "scopes.userWrite.label": { - "message": "Write user data" - }, - "scopes.versionCreate.description": { - "message": "Create new versions" - }, - "scopes.versionCreate.label": { - "message": "Create versions" - }, - "scopes.versionDelete.description": { - "message": "Delete a version" - }, - "scopes.versionDelete.label": { - "message": "Delete versions" - }, - "scopes.versionRead.description": { - "message": "Read all versions" - }, - "scopes.versionRead.label": { - "message": "Read versions" - }, - "scopes.versionWrite.description": { - "message": "Write to version data" - }, - "scopes.versionWrite.label": { - "message": "Write versions" - }, - "settings.language.categories.auto": { - "message": "Automatic" - }, - "settings.language.categories.default": { - "message": "Standard languages" - }, - "settings.language.categories.experimental": { - "message": "Experimental languages" - }, - "settings.language.categories.fun": { - "message": "Fun languages" - }, - "settings.language.categories.search-result": { - "message": "Search results" - }, - "settings.language.description": { - "message": "Choose your preferred language for the site. Translations are contributed by volunteers on Crowdin." - }, - "settings.language.languages.automatic": { - "message": "Sync with the system language" - }, - "settings.language.languages.language-label-applying": { - "message": "{label}. Applying..." - }, - "settings.language.languages.language-label-error": { - "message": "{label}. Error" - }, - "settings.language.languages.load-failed": { - "message": "Cannot load this language. Try again in a bit." - }, - "settings.language.languages.search-field.description": { - "message": "Submit to focus the first search result" - }, - "settings.language.languages.search-field.placeholder": { - "message": "Search for a language..." - }, - "settings.language.languages.search-results-announcement": { - "message": "{matches, plural, =0 {No languages match} one {# language matches} other {# languages match}} your search." - }, - "settings.language.languages.search.no-results": { - "message": "No languages match your search." - }, - "settings.language.title": { - "message": "Language" - }, - "settings.pats.action.create": { - "message": "Create a PAT" - }, - "settings.pats.description": { - "message": "PATs can be used to access Modrinth's API. For more information, see Modrinth's API documentation. They can be created and revoked at any time." - }, - "settings.pats.modal.create.action": { - "message": "Create PAT" - }, - "settings.pats.modal.create.expires.label": { - "message": "Expires" - }, - "settings.pats.modal.create.name.label": { - "message": "Name" - }, - "settings.pats.modal.create.name.placeholder": { - "message": "Enter the PAT's name..." - }, - "settings.pats.modal.create.title": { - "message": "Create personal access token" - }, - "settings.pats.modal.delete.action": { - "message": "Delete this token" - }, - "settings.pats.modal.delete.description": { - "message": "This will remove this token forever (like really forever)." - }, - "settings.pats.modal.delete.title": { - "message": "Are you sure you want to delete this token?" - }, - "settings.pats.modal.edit.title": { - "message": "Edit personal access token" - }, - "settings.pats.title": { - "message": "PATs" - }, - "settings.pats.title.long": { - "message": "Personal Access Tokens" - }, - "settings.pats.token.action.edit": { - "message": "Edit token" - }, - "settings.pats.token.action.revoke": { - "message": "Revoke token" - }, - "settings.pats.token.expired-ago": { - "message": "Expired {ago}" - }, - "settings.pats.token.expires-in": { - "message": "Expires {inTime}" - }, - "settings.pats.token.last-used": { - "message": "Last used {ago}" - }, - "settings.pats.token.never-used": { - "message": "Never used" - }, - "settings.sessions.action.revoke-session": { - "message": "Revoke session" - }, - "settings.sessions.created-ago": { - "message": "Created {ago}" - }, - "settings.sessions.current-session": { - "message": "Current session" - }, - "settings.sessions.description": { - "message": "Here are all the devices that are currently logged in with your Modrinth account. You can log out of each one individually.\n\nIf you see an entry you don't recognize, log out of that device and change your Modrinth account password immediately." - }, - "settings.sessions.last-accessed-ago": { - "message": "Last accessed {ago}" - }, - "settings.sessions.title": { - "message": "Sessions" - }, - "settings.sessions.unknown-os": { - "message": "Unknown OS" - }, - "settings.sessions.unknown-platform": { - "message": "Unknown platform" - }, - "tooltip.date-at-time": { - "message": "{date, date, long} at {time, time, short}" - } -} diff --git a/package.json b/package.json index 349d84053e..aa31f22ee8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "build": "nuxi build", + "build": "pnpm intl:extract && nuxi build", "dev": "nuxi dev", "generate": "nuxi generate", "preview": "nuxi preview",