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