mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
ci(client): add Crowdin translation workflow
Establish native GitHub synchronization, scoped translation PR checks, locale safeguards, semantic-release handling, and contributor guidance.
This commit is contained in:
@@ -50,8 +50,33 @@ jobs:
|
|||||||
issues: read
|
issues: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout trusted pull request base
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|
||||||
|
- name: Classify Crowdin pull request
|
||||||
|
id: crowdin
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
BASE_REPO: ${{ github.repository }}
|
||||||
|
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
HEAD_BRANCH: ${{ github.head_ref }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
args=("$PR_BASE_SHA" "$PR_HEAD_SHA" "$BASE_REPO" "$HEAD_REPO" "$HEAD_BRANCH")
|
||||||
|
if git cat-file -e "$PR_BASE_SHA:scripts/classify-crowdin-pr.sh" 2>/dev/null; then
|
||||||
|
is_crowdin="$(git show "$PR_BASE_SHA:scripts/classify-crowdin-pr.sh" | bash -s -- "${args[@]}")"
|
||||||
|
else
|
||||||
|
is_crowdin="$(git show "$PR_HEAD_SHA:scripts/classify-crowdin-pr.sh" | bash -s -- "${args[@]}")"
|
||||||
|
fi
|
||||||
|
echo "is_crowdin=$is_crowdin" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Validate PR branch name
|
- name: Validate PR branch name
|
||||||
if: github.event.pull_request.user.login != 'dependabot[bot]'
|
if: github.event.pull_request.user.login != 'dependabot[bot]' && steps.crowdin.outputs.is_crowdin != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
PR_BRANCH: ${{ github.head_ref }}
|
PR_BRANCH: ${{ github.head_ref }}
|
||||||
@@ -65,7 +90,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Validate referenced issues exist
|
- name: Validate referenced issues exist
|
||||||
if: github.event.pull_request.user.login != 'dependabot[bot]'
|
if: github.event.pull_request.user.login != 'dependabot[bot]' && steps.crowdin.outputs.is_crowdin != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_REPO: ${{ github.repository }}
|
GH_REPO: ${{ github.repository }}
|
||||||
|
|||||||
@@ -30,6 +30,31 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|
||||||
|
- name: Classify Crowdin pull request
|
||||||
|
id: crowdin
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
BASE_REPO: ${{ github.repository }}
|
||||||
|
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
HEAD_BRANCH: ${{ github.head_ref }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
args=("$PR_BASE_SHA" "$PR_HEAD_SHA" "$BASE_REPO" "$HEAD_REPO" "$HEAD_BRANCH")
|
||||||
|
if git cat-file -e "$PR_BASE_SHA:scripts/classify-crowdin-pr.sh" 2>/dev/null; then
|
||||||
|
is_crowdin="$(git show "$PR_BASE_SHA:scripts/classify-crowdin-pr.sh" | bash -s -- "${args[@]}")"
|
||||||
|
else
|
||||||
|
is_crowdin="$(git show "$PR_HEAD_SHA:scripts/classify-crowdin-pr.sh" | bash -s -- "${args[@]}")"
|
||||||
|
fi
|
||||||
|
echo "is_crowdin=$is_crowdin" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Checkout pull request
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -54,7 +79,7 @@ jobs:
|
|||||||
run: pnpm exec commitlint --config commitlint.config.cjs --from "$PR_BASE_SHA" --to "$PR_HEAD_SHA" --verbose
|
run: pnpm exec commitlint --config commitlint.config.cjs --from "$PR_BASE_SHA" --to "$PR_HEAD_SHA" --verbose
|
||||||
|
|
||||||
- name: Validate referenced issues exist
|
- name: Validate referenced issues exist
|
||||||
if: github.event.pull_request.user.login != 'dependabot[bot]'
|
if: github.event.pull_request.user.login != 'dependabot[bot]' && steps.crowdin.outputs.is_crowdin != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_REPO: ${{ github.repository }}
|
GH_REPO: ${{ github.repository }}
|
||||||
|
|||||||
@@ -86,10 +86,15 @@ for (const [locale, catalog] of catalogs) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (message.length === 0) errors.push(`${locale}: empty message ${key}`)
|
if (message.length === 0) errors.push(`${locale}: empty message ${key}`)
|
||||||
|
if (message.includes('\u2014')) errors.push(`${locale}: Unicode em dash is not allowed in ${key}`)
|
||||||
if (/<[^>]+>/.test(message)) errors.push(`${locale}: HTML is not allowed in ${key}`)
|
if (/<[^>]+>/.test(message)) errors.push(`${locale}: HTML is not allowed in ${key}`)
|
||||||
if (placeholders(message).join(',') !== placeholders(referenceMessage).join(',')) {
|
if (placeholders(message).join(',') !== placeholders(referenceMessage).join(',')) {
|
||||||
errors.push(`${locale}: placeholders differ for ${key}`)
|
errors.push(`${locale}: placeholders differ for ${key}`)
|
||||||
}
|
}
|
||||||
|
const referenceIsPlural = referenceMessage.includes(' | ')
|
||||||
|
const messageIsPlural = message.includes(' | ')
|
||||||
|
if (referenceIsPlural && !messageIsPlural) errors.push(`${locale}: plural branches missing for ${key}`)
|
||||||
|
if (!referenceIsPlural && messageIsPlural) errors.push(`${locale}: unexpected plural branches in ${key}`)
|
||||||
if (message.includes(' | ') && message.split(' | ').some((branch) => branch.trim().length === 0)) {
|
if (message.includes(' | ') && message.split(' | ').some((branch) => branch.trim().length === 0)) {
|
||||||
errors.push(`${locale}: empty plural branch in ${key}`)
|
errors.push(`${locale}: empty plural branch in ${key}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["@commitlint/config-conventional"],
|
extends: ["@commitlint/config-conventional"],
|
||||||
rules: {
|
rules: {
|
||||||
"type-enum": [2, "always", ["feat", "fix", "db", "perf", "refactor", "style", "docs", "test", "build", "ci", "chore", "security", "revert"]],
|
"type-enum": [
|
||||||
|
2,
|
||||||
|
"always",
|
||||||
|
["feat", "fix", "i18n", "db", "perf", "refactor", "style", "docs", "test", "build", "ci", "chore", "security", "revert"],
|
||||||
|
],
|
||||||
"header-max-length": [2, "always", 100],
|
"header-max-length": [2, "always", 100],
|
||||||
"body-max-line-length": [2, "always", 500],
|
"body-max-line-length": [2, "always", 500],
|
||||||
"subject-full-stop": [2, "never", "."],
|
"subject-full-stop": [2, "never", "."],
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
preserve_hierarchy: true
|
||||||
|
|
||||||
|
export_languages:
|
||||||
|
- de
|
||||||
|
- nl
|
||||||
|
- pt-BR
|
||||||
|
- sl
|
||||||
|
|
||||||
|
pull_request_title: "i18n(client): sync translations from Crowdin"
|
||||||
|
commit_message: |-
|
||||||
|
i18n(client): sync %language% translations from Crowdin
|
||||||
|
|
||||||
|
Keep translated catalogs synchronized with reviewed Crowdin content.
|
||||||
|
append_commit_message: false
|
||||||
|
|
||||||
|
files:
|
||||||
|
- source: /client/src/locales/en.json
|
||||||
|
translation: /client/src/locales/%two_letters_code%.json
|
||||||
|
update_option: update_as_unapproved
|
||||||
@@ -32,7 +32,7 @@ The **footer** is optional. Use it to reference issues, close PRs, or document b
|
|||||||
│ users|stats|notifications|settings|scanner|
|
│ users|stats|notifications|settings|scanner|
|
||||||
│ email|audit|smart-scope|types|docker|deps|server|client
|
│ email|audit|smart-scope|types|docker|deps|server|client
|
||||||
│
|
│
|
||||||
└─ Type: feat|fix|db|perf|refactor|style|docs|test|build|ci|chore|security|revert
|
└─ Type: feat|fix|i18n|db|perf|refactor|style|docs|test|build|ci|chore|security|revert
|
||||||
```
|
```
|
||||||
|
|
||||||
The `<type>` and `<summary>` fields are required. The `(<scope>)` field is optional.
|
The `<type>` and `<summary>` fields are required. The `(<scope>)` field is optional.
|
||||||
@@ -45,6 +45,7 @@ The type signals the intent of the change at a glance.
|
|||||||
| ---------- | -------------------------------------------------------------------------------------------------------- |
|
| ---------- | -------------------------------------------------------------------------------------------------------- |
|
||||||
| `feat` | Introduces a new feature or user-visible capability |
|
| `feat` | Introduces a new feature or user-visible capability |
|
||||||
| `fix` | Patches a bug in existing behavior |
|
| `fix` | Patches a bug in existing behavior |
|
||||||
|
| `i18n` | Adds or updates translations and localization catalogs |
|
||||||
| `db` | Adds or modifies a Drizzle schema, generates a migration, or changes seed data |
|
| `db` | Adds or modifies a Drizzle schema, generates a migration, or changes seed data |
|
||||||
| `perf` | Improves performance without changing observable behavior |
|
| `perf` | Improves performance without changing observable behavior |
|
||||||
| `refactor` | Restructures code without fixing bugs or adding features |
|
| `refactor` | Restructures code without fixing bugs or adding features |
|
||||||
@@ -195,6 +196,12 @@ elapsed time.
|
|||||||
Fixes #412
|
Fixes #412
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `i18n`
|
||||||
|
|
||||||
|
```
|
||||||
|
i18n(client): sync German translations from Crowdin
|
||||||
|
```
|
||||||
|
|
||||||
### `db`
|
### `db`
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Everything you need to work in this codebase: architecture, setup, workflows, conventions, and commands.
|
Everything you need to work in this codebase: architecture, setup, workflows, conventions, and commands.
|
||||||
|
|
||||||
For the contribution process (issues, PRs, review), see [CONTRIBUTING.md](CONTRIBUTING.md). For test architecture and E2E suite details, see [TESTING.md](TESTING.md).
|
For the contribution process (issues, PRs, review), see [CONTRIBUTING.md](CONTRIBUTING.md). For localization and Crowdin workflow details, see [LOCALIZATION.md](LOCALIZATION.md). For test architecture and E2E suite details, see [TESTING.md](TESTING.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Localization
|
||||||
|
|
||||||
|
BookOrbit uses Vue I18n catalogs under `client/src/locales/`. English is the source language, and Crowdin is the source of truth for German, Dutch, Brazilian Portuguese, and Slovenian translations.
|
||||||
|
|
||||||
|
## Supported Catalogs
|
||||||
|
|
||||||
|
| Application locale | Crowdin language ID | Catalog |
|
||||||
|
| -------------------- | ------------------- | --------- |
|
||||||
|
| English | Source language | `en.json` |
|
||||||
|
| German | `de` | `de.json` |
|
||||||
|
| Dutch | `nl` | `nl.json` |
|
||||||
|
| Brazilian Portuguese | `pt-BR` | `pt.json` |
|
||||||
|
| Slovenian | `sl` | `sl.json` |
|
||||||
|
|
||||||
|
Crowdin's `%two_letters_code%` placeholder maps Brazilian Portuguese to `pt`, which matches BookOrbit's existing application locale and filename.
|
||||||
|
|
||||||
|
## Adding User-Facing Copy
|
||||||
|
|
||||||
|
Feature branches must:
|
||||||
|
|
||||||
|
1. Add the source message to `client/src/locales/en.json`.
|
||||||
|
2. Add the same English value under the same key in every target catalog as a temporary fallback.
|
||||||
|
3. Preserve every placeholder and the plural-message structure.
|
||||||
|
4. Run `pnpm --filter client validate:locales`.
|
||||||
|
|
||||||
|
Do not create or improve target-language translations directly in Git. Crowdin does not continuously import repository translations after initial setup, so a later Crowdin export would overwrite those changes. Apply any emergency target-catalog fix in Crowdin before the next export.
|
||||||
|
|
||||||
|
When an English edit changes meaning rather than wording, prefer a new message key. If the key must remain, explicitly invalidate or clear the affected translations in Crowdin so an outdated translation is not exported for the new meaning.
|
||||||
|
|
||||||
|
## Plural Messages
|
||||||
|
|
||||||
|
Vue I18n plural messages use pipe-separated branches:
|
||||||
|
|
||||||
|
```text
|
||||||
|
No books | 1 book | {count} books
|
||||||
|
```
|
||||||
|
|
||||||
|
Crowdin's JSON parser treats this syntax as ordinary text. Translators must preserve the pipe-separated structure and placeholders manually. Locale branch counts can differ; Slovenian commonly needs more branches than English.
|
||||||
|
|
||||||
|
Repository validation rejects missing plural structure, empty plural branches, placeholder mismatches, embedded HTML, and Unicode em dash characters. Human review is still required for locale-specific plural semantics.
|
||||||
|
|
||||||
|
## Translation Delivery
|
||||||
|
|
||||||
|
After an English source change reaches `main`:
|
||||||
|
|
||||||
|
1. Crowdin synchronizes `client/src/locales/en.json`.
|
||||||
|
2. Translators update target strings in Crowdin.
|
||||||
|
3. Crowdin exports the target catalogs to `l10n_main`.
|
||||||
|
4. Crowdin opens a pull request to `main`.
|
||||||
|
5. CI verifies that the pull request changes only the four target catalogs and runs the normal client checks.
|
||||||
|
6. A maintainer reviews and squash-merges the pull request.
|
||||||
|
7. The `l10n_main` service branch is deleted. Crowdin recreates it for the next export.
|
||||||
|
|
||||||
|
Crowdin pull requests must retain the configured `i18n(client)` title and commit format. The `i18n` commit type produces a patch release and an Internationalization release-note section. Crowdin's default `[ci skip]` commit suffix is disabled so pull-request validation runs before merge.
|
||||||
|
|
||||||
|
## Adding a New Language
|
||||||
|
|
||||||
|
Add languages through a normal issue-linked pull request before enabling their Crowdin export. This ensures the application, validation, and Crowdin PR allowlist are ready before Crowdin creates the first translation update.
|
||||||
|
|
||||||
|
### 1. Choose the locale identifiers
|
||||||
|
|
||||||
|
Choose both:
|
||||||
|
|
||||||
|
- The application locale and catalog filename, preferably a canonical BCP 47 identifier such as `fr` or `fr-CA`.
|
||||||
|
- The exact Crowdin language ID shown in Crowdin's language-code reference.
|
||||||
|
|
||||||
|
Crowdin's `export_languages` entries use Crowdin language IDs, while `%two_letters_code%` controls the exported filename. If two enabled variants share the same two-letter code, or the application identifier differs from Crowdin's two-letter code, add a per-file `languages_mapping` entry in `crowdin.yml` so exports cannot collide. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
files:
|
||||||
|
- source: /client/src/locales/en.json
|
||||||
|
translation: /client/src/locales/%two_letters_code%.json
|
||||||
|
languages_mapping:
|
||||||
|
two_letters_code:
|
||||||
|
fr-CA: fr-CA
|
||||||
|
```
|
||||||
|
|
||||||
|
Never enable two Crowdin languages that resolve to the same catalog path.
|
||||||
|
|
||||||
|
### 2. Register the application locale
|
||||||
|
|
||||||
|
Update `packages/types/src/locale.ts`:
|
||||||
|
|
||||||
|
- Add the application locale to `SUPPORTED_LOCALES`.
|
||||||
|
- Add its native-language display name to `LOCALE_LABELS`.
|
||||||
|
- Set `ltr` or `rtl` in `LOCALE_DIRECTIONS`.
|
||||||
|
|
||||||
|
The shared locale list automatically updates the language picker, server preference validation, and client locale typing. For an RTL language, manually verify the document direction and responsive layouts in addition to setting `rtl`.
|
||||||
|
|
||||||
|
### 3. Add the catalog and locale behavior
|
||||||
|
|
||||||
|
Create `client/src/locales/<locale>.json` with the exact key structure from `en.json` and English fallback values. Do not add partial catalogs. Crowdin will replace fallback values as translations are completed.
|
||||||
|
|
||||||
|
Review `client/src/stores/locale.ts` and add browser-locale matching coverage to `client/src/stores/__tests__/locale.spec.ts`, especially for regional variants. Vue I18n supplies standard locale pluralization; if the language needs a project-specific rule, register it in `client/src/i18n/index.ts` and add focused tests.
|
||||||
|
|
||||||
|
### 4. Allow Crowdin export and PR delivery
|
||||||
|
|
||||||
|
Update `crowdin.yml`:
|
||||||
|
|
||||||
|
- Add the exact Crowdin language ID to `export_languages`.
|
||||||
|
- Add `languages_mapping` when the desired filename is not the language's unique `%two_letters_code%` output.
|
||||||
|
|
||||||
|
Update `scripts/classify-crowdin-pr.sh` to allow the new target catalog path. Do not allow `en.json` or broaden the rule to the whole locales directory.
|
||||||
|
|
||||||
|
Before merging, add the target language in Crowdin while the current `main` configuration still excludes it from `export_languages`, or pause translation synchronization. Merge the repository support before allowing the first export. After merge, run a manual source and translation sync and confirm that the generated PR changes only explicitly allowed target catalogs.
|
||||||
|
|
||||||
|
### 5. Verify the new language
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @bookorbit/types build
|
||||||
|
pnpm --filter client validate:locales
|
||||||
|
pnpm --filter client lint:check
|
||||||
|
pnpm --filter server lint:check
|
||||||
|
pnpm typecheck:server
|
||||||
|
pnpm typecheck:client
|
||||||
|
pnpm --filter client exec vitest run src/stores/__tests__/locale.spec.ts
|
||||||
|
pnpm --filter server exec vitest run src/modules/user-preferences/user-preferences.service.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Then verify manually:
|
||||||
|
|
||||||
|
- The language picker shows the native label.
|
||||||
|
- Browser detection selects the intended locale without confusing regional variants.
|
||||||
|
- Refreshing the application preserves the selected locale.
|
||||||
|
- `<html lang>` and `<html dir>` are correct.
|
||||||
|
- Number, date, and relative-time formatting use the intended locale.
|
||||||
|
- Representative desktop and mobile screens handle translated text expansion.
|
||||||
|
- RTL layout, keyboard navigation, focus states, dialogs, menus, and popovers remain usable when applicable.
|
||||||
|
- A manual Crowdin export produces the expected filename and passes all PR checks.
|
||||||
|
|
||||||
|
Only enable scheduled export for the new language after this manual round trip succeeds.
|
||||||
|
|
||||||
|
## Crowdin Project Settings
|
||||||
|
|
||||||
|
Use the native GitHub integration in **Source and translation files mode** with only `main` connected. Do not use Target file bundles mode or automatic feature-branch discovery.
|
||||||
|
|
||||||
|
Initial import settings:
|
||||||
|
|
||||||
|
- Import existing translations once.
|
||||||
|
- Allow target translations to match the source.
|
||||||
|
- Do not continuously import translations from GitHub afterward.
|
||||||
|
- Keep Push Sources disabled.
|
||||||
|
|
||||||
|
Export settings:
|
||||||
|
|
||||||
|
- Skip untranslated strings: off.
|
||||||
|
- Skip untranslated files: off.
|
||||||
|
- Export only approved translations: off during the initial rollout.
|
||||||
|
|
||||||
|
These settings make Crowdin export English source text for untranslated entries instead of empty JSON values, preserving BookOrbit's complete-catalog requirement.
|
||||||
|
|
||||||
|
Configure variable mismatches and leading or trailing whitespace as Crowdin QA errors. Keep punctuation and length checks enabled, and include translator instructions for Vue I18n plural branches, the HTML prohibition, and the Unicode em dash prohibition.
|
||||||
|
|
||||||
|
Use a daily translation export schedule and manual synchronization before releases. Keep automatic merging disabled.
|
||||||
@@ -7,6 +7,7 @@ const DOCKER_IMAGE = "ghcr.io/bookorbit/bookorbit";
|
|||||||
const TYPES = [
|
const TYPES = [
|
||||||
{ type: "feat", section: "Features" },
|
{ type: "feat", section: "Features" },
|
||||||
{ type: "fix", section: "Bug Fixes" },
|
{ type: "fix", section: "Bug Fixes" },
|
||||||
|
{ type: "i18n", section: "Internationalization" },
|
||||||
{ type: "security", section: "Security" },
|
{ type: "security", section: "Security" },
|
||||||
{ type: "perf", section: "Performance" },
|
{ type: "perf", section: "Performance" },
|
||||||
{ type: "db", section: "Database" },
|
{ type: "db", section: "Database" },
|
||||||
@@ -199,6 +200,7 @@ module.exports = {
|
|||||||
releaseRules: [
|
releaseRules: [
|
||||||
{ type: "feat", release: "minor" },
|
{ type: "feat", release: "minor" },
|
||||||
{ type: "fix", release: "patch" },
|
{ type: "fix", release: "patch" },
|
||||||
|
{ type: "i18n", release: "patch" },
|
||||||
{ type: "perf", release: "patch" },
|
{ type: "perf", release: "patch" },
|
||||||
{ type: "security", release: "patch" },
|
{ type: "security", release: "patch" },
|
||||||
{ type: "db", release: "patch" },
|
{ type: "db", release: "patch" },
|
||||||
|
|||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ "$#" -ne 5 ]]; then
|
||||||
|
echo "usage: $0 <base-sha> <head-sha> <base-repo> <head-repo> <head-branch>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE_SHA="$1"
|
||||||
|
HEAD_SHA="$2"
|
||||||
|
BASE_REPO="$3"
|
||||||
|
HEAD_REPO="$4"
|
||||||
|
HEAD_BRANCH="$5"
|
||||||
|
|
||||||
|
if [[ "$HEAD_REPO" != "$BASE_REPO" || "$HEAD_BRANCH" != "l10n_main" ]]; then
|
||||||
|
echo "false"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
allowed_paths=(
|
||||||
|
"client/src/locales/de.json"
|
||||||
|
"client/src/locales/nl.json"
|
||||||
|
"client/src/locales/pt.json"
|
||||||
|
"client/src/locales/sl.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
is_allowed_path() {
|
||||||
|
local candidate="$1"
|
||||||
|
local allowed
|
||||||
|
|
||||||
|
for allowed in "${allowed_paths[@]}"; do
|
||||||
|
if [[ "$candidate" == "$allowed" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
changed_paths=()
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
changed_paths+=("$path")
|
||||||
|
done < <(git diff --name-only -z "$BASE_SHA" "$HEAD_SHA")
|
||||||
|
|
||||||
|
if [[ "${#changed_paths[@]}" -eq 0 ]]; then
|
||||||
|
echo "::error::Crowdin pull request does not contain any changed translation catalogs." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for path in "${changed_paths[@]}"; do
|
||||||
|
if ! is_allowed_path "$path"; then
|
||||||
|
echo "::error::Crowdin pull request contains disallowed path '$path'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mode="$(git ls-tree "$HEAD_SHA" -- "$path" | awk '{print $1}')"
|
||||||
|
if [[ "$mode" != "100644" ]]; then
|
||||||
|
echo "::error::Crowdin pull request must retain '$path' as a regular non-executable file." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "true"
|
||||||
Reference in New Issue
Block a user