chore: GitHub actions for translation syntax validation and docs link (#23627)
* chore: Add GA for translation syntax validation
* chore: Documentation link checker
Co-authored-by: Gavin D'souza <gavin18d@gmail.com>
* fix: URL
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
Co-authored-by: Gavin D'souza <gavin18d@gmail.com>
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py
new file mode 100644
index 0000000..b603ed5
--- /dev/null
+++ b/.github/helper/documentation.py
@@ -0,0 +1,48 @@
+import sys
+import requests
+from urllib.parse import urlparse
+
+
+docs_repos = [
+ "frappe_docs",
+ "erpnext_documentation",
+ "erpnext_com",
+ "frappe_io",
+]
+
+
+def uri_validator(x):
+ result = urlparse(x)
+ return all([result.scheme, result.netloc, result.path])
+
+def docs_link_exists(body):
+ for line in body.splitlines():
+ for word in line.split():
+ if word.startswith('http') and uri_validator(word):
+ parsed_url = urlparse(word)
+ if parsed_url.netloc == "github.com":
+ _, org, repo, _type, ref = parsed_url.path.split('/')
+ if org == "frappe" and repo in docs_repos:
+ return True
+
+
+if __name__ == "__main__":
+ pr = sys.argv[1]
+ response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
+
+ if response.ok:
+ payload = response.json()
+ title = payload.get("title", "").lower()
+ head_sha = payload.get("head", {}).get("sha")
+ body = payload.get("body", "").lower()
+
+ if title.startswith("feat") and head_sha and "no-docs" not in body:
+ if docs_link_exists(body):
+ print("Documentation Link Found. You're Awesome! 🎉")
+
+ else:
+ print("Documentation Link Not Found! ⚠️")
+ sys.exit(1)
+
+ else:
+ print("Skipping documentation checks... 🏃")
diff --git a/.github/helper/translation.py b/.github/helper/translation.py
new file mode 100644
index 0000000..340f4f8
--- /dev/null
+++ b/.github/helper/translation.py
@@ -0,0 +1,60 @@
+import re
+import sys
+
+errors_encounter = 0
+pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
+words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]")
+start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}")
+f_string_pattern = re.compile(r"_\(f[\"']")
+starts_with_f_pattern = re.compile(r"_\(f")
+
+# skip first argument
+files = sys.argv[1:]
+files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))]
+
+for _file in files_to_scan:
+ with open(_file, 'r') as f:
+ print(f'Checking: {_file}')
+ file_lines = f.readlines()
+ for line_number, line in enumerate(file_lines, 1):
+ if 'frappe-lint: disable-translate' in line:
+ continue
+
+ start_matches = start_pattern.search(line)
+ if start_matches:
+ starts_with_f = starts_with_f_pattern.search(line)
+
+ if starts_with_f:
+ has_f_string = f_string_pattern.search(line)
+ if has_f_string:
+ errors_encounter += 1
+ print(f'\nF-strings are not supported for translations at line number {line_number + 1}\n{line.strip()[:100]}')
+ continue
+ else:
+ continue
+
+ match = pattern.search(line)
+ error_found = False
+
+ if not match and line.endswith(',\n'):
+ # concat remaining text to validate multiline pattern
+ line = "".join(file_lines[line_number - 1:])
+ line = line[start_matches.start() + 1:]
+ match = pattern.match(line)
+
+ if not match:
+ error_found = True
+ print(f'\nTranslation syntax error at line number {line_number + 1}\n{line.strip()[:100]}')
+
+ if not error_found and not words_pattern.search(line):
+ error_found = True
+ print(f'\nTranslation is useless because it has no words at line number {line_number + 1}\n{line.strip()[:100]}')
+
+ if error_found:
+ errors_encounter += 1
+
+if errors_encounter > 0:
+ print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.')
+ sys.exit(1)
+else:
+ print('\nGood To Go!')
diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml
new file mode 100644
index 0000000..cdf676d
--- /dev/null
+++ b/.github/workflows/docs-checker.yml
@@ -0,0 +1,24 @@
+name: 'Documentation Required'
+on:
+ pull_request:
+ types: [ opened, synchronize, reopened, edited ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: 'Setup Environment'
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.6
+
+ - name: 'Clone repo'
+ uses: actions/checkout@v2
+
+ - name: Validate Docs
+ env:
+ PR_NUMBER: ${{ github.event.number }}
+ run: |
+ pip install requests --quiet
+ python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER
diff --git a/.github/workflows/translation_linter.yml b/.github/workflows/translation_linter.yml
new file mode 100644
index 0000000..4becaeb
--- /dev/null
+++ b/.github/workflows/translation_linter.yml
@@ -0,0 +1,22 @@
+name: Frappe Linter
+on:
+ pull_request:
+ branches:
+ - develop
+ - version-12-hotfix
+ - version-11-hotfix
+jobs:
+ check_translation:
+ name: Translation Syntax Check
+ runs-on: ubuntu-18.04
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup python3
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.6
+ - name: Validating Translation Syntax
+ run: |
+ git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
+ files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
+ python $GITHUB_WORKSPACE/.github/helper/translation.py $files