Skip to content

Commit c3b88e7

Browse files
authored
Merge branch 'main' into main
2 parents 1728e36 + bff0b43 commit c3b88e7

82 files changed

Lines changed: 20494 additions & 1408 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/i18n-auto.yml

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
name: i18n-auto-translate
2+
on:
3+
push:
4+
branches: [ main, feature/**, chore/** ]
5+
pull_request:
6+
branches: [ main ]
7+
workflow_dispatch: {}
8+
jobs:
9+
i18n:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
pull-requests: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Install gettext
20+
run: |
21+
sudo apt-get update \
22+
&& sudo apt-get install -y gettext
23+
24+
- uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.11"
27+
28+
- name: Install Python deps
29+
run: |
30+
python -m pip install --upgrade pip \
31+
&& pip install -r requirements.txt \
32+
&& pip install polib deepl
33+
34+
- name: Make messages (django & djangojs)
35+
run: |
36+
python manage.py makemessages --no-wrap -l zh_Hans -l fr -l es -l ja -l ko \
37+
&& python manage.py makemessages --no-wrap -l zh_Hans -l fr -l es -l ja -l ko -d djangojs
38+
39+
- name: Decide whether DeepL is available
40+
id: deepl_guard
41+
env:
42+
HAS_DEEPL: ${{ secrets.DEEPL_API_KEY != '' }}
43+
run: |
44+
echo "has_deepl=${HAS_DEEPL}" >> "$GITHUB_OUTPUT"
45+
46+
- name: Auto translate (DeepL)
47+
if: ${{ github.event_name != 'pull_request' && steps.deepl_guard.outputs.has_deepl == 'true' }}
48+
env:
49+
DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }}
50+
run: |
51+
python manage.py auto_translate_messages --locale_dir=locale
52+
53+
- name: Skip DeepL (PR build or no secret)
54+
if: ${{ !(github.event_name != 'pull_request' && steps.deepl_guard.outputs.has_deepl == 'true') }}
55+
run: echo "Skipping DeepL (pull_request build or no DEEPL_API_KEY)."
56+
57+
- name: Normalise PO newline parity
58+
run: |
59+
python - <<'PY'
60+
import polib, pathlib
61+
base = pathlib.Path("locale")
62+
for po_path in base.rglob("*/LC_MESSAGES/*.po"):
63+
po = polib.pofile(str(po_path))
64+
changed = False
65+
def align(msgid, s):
66+
if s is None: return s
67+
out = s
68+
if msgid.startswith('\n') and not out.startswith('\n'): out = '\n' + out
69+
if not msgid.startswith('\n') and out.startswith('\n'): out = out.lstrip('\n')
70+
if msgid.endswith('\n') and not out.endswith('\n'): out = out + '\n'
71+
if not msgid.endswith('\n') and out.endswith('\n'): out = out.rstrip('\n')
72+
return out
73+
for e in po:
74+
if e.msgstr:
75+
e.msgstr = align(e.msgid, e.msgstr); changed = True
76+
if e.msgstr_plural:
77+
for k,v in e.msgstr_plural.items():
78+
e.msgstr_plural[k] = align(e.msgid, v); changed = True
79+
if changed:
80+
po.save(str(po_path))
81+
PY
82+
83+
- name: Repair Python-format placeholders in PO files
84+
run: |
85+
python - <<'PY'
86+
import re, pathlib, polib
87+
base = pathlib.Path("locale")
88+
89+
# Match Python %-style placeholders: %s, %d, %(name)s, etc. (ignore %%)
90+
PCT = re.compile(r'%(?:\(\w+\))?[#0\- +]?\d*(?:\.\d+)?[diouxXeEfFgGcrs%]')
91+
92+
def tokens(text: str):
93+
toks = []
94+
for t in PCT.findall(text or ''):
95+
if t == '%%':
96+
continue
97+
# normalise named tokens to just the name + type, e.g. %(name)s -> ('name','s')
98+
if t.startswith('%('):
99+
name = t[t.find('(')+1:t.find(')')]
100+
typ = t[-1]
101+
toks.append(('named', name, typ))
102+
else:
103+
toks.append(('positional', None, t[-1]))
104+
return toks
105+
106+
def needs_fix(id_tokens, s):
107+
return s is not None and tokens(s) != id_tokens
108+
109+
for po_path in base.rglob("*/LC_MESSAGES/*.po"):
110+
po = polib.pofile(str(po_path))
111+
dirty = False
112+
for e in po:
113+
id_tokens = tokens(e.msgid)
114+
pl_tokens = tokens(e.msgid_plural) if e.msgid_plural else None
115+
116+
# singular
117+
if e.msgstr and id_tokens and needs_fix(id_tokens, e.msgstr):
118+
# safest repair: copy msgid so placeholders are correct
119+
e.msgstr = e.msgid
120+
dirty = True
121+
122+
# plural forms
123+
if e.msgid_plural and e.msgstr_plural:
124+
# Prefer tokens from msgid_plural if present, else fallback to singular tokens
125+
want = pl_tokens if pl_tokens else id_tokens
126+
if want:
127+
for k, v in list(e.msgstr_plural.items()):
128+
if needs_fix(want, v):
129+
e.msgstr_plural[k] = e.msgid_plural or e.msgid
130+
dirty = True
131+
132+
if dirty:
133+
po.save(str(po_path))
134+
PY
135+
136+
- name: Compile messages (tolerant, per-locale)
137+
run: |
138+
set +e
139+
for L in zh_Hans fr es ja ko; do \
140+
echo "Compiling locale: $L"; \
141+
python manage.py compilemessages \
142+
-l "$L" \
143+
-i venv -i .git -i node_modules -i "**/site-packages/**"; \
144+
if [ "$?" -ne 0 ]; then \
145+
echo "::warning::Skipping $L due to compile error after auto-repair"; \
146+
fi; \
147+
done
148+
set -e
149+
150+
- name: Check for changes
151+
id: changes
152+
if: ${{ github.ref == 'refs/heads/main' }}
153+
run: |
154+
if git diff --quiet HEAD -- locale/; then
155+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
156+
echo "No translation changes detected"
157+
else
158+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
159+
echo "Translation changes detected"
160+
fi
161+
162+
- name: Check if branch exists and commit changes
163+
if: ${{ github.ref == 'refs/heads/main' && steps.changes.outputs.has_changes == 'true' }}
164+
id: commit_changes
165+
run: |
166+
git config user.name "i18n-bot"
167+
git config user.email "i18n-bot@example.com"
168+
BRANCH_NAME="chore/i18n-auto-updates"
169+
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then
170+
echo "Branch $BRANCH_NAME exists, checking out and updating..."
171+
git fetch origin "$BRANCH_NAME" \
172+
&& git checkout "$BRANCH_NAME" \
173+
&& git merge origin/main --no-edit
174+
else
175+
echo "Branch $BRANCH_NAME doesn't exist, creating new branch..."
176+
git checkout -b "$BRANCH_NAME"
177+
fi
178+
git add locale/**/LC_MESSAGES/*.po locale/**/LC_MESSAGES/*.mo \
179+
&& git commit -m "chore(i18n): auto-translate & normalise & compile [$(date '+%Y-%m-%d %H:%M')]" || {
180+
echo "No changes to commit"; exit 0; }
181+
git push -u origin "$BRANCH_NAME"
182+
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
183+
184+
- name: Check if PR exists
185+
if: ${{ github.ref == 'refs/heads/main' && steps.changes.outputs.has_changes == 'true' }}
186+
id: check_pr
187+
run: |
188+
BRANCH_NAME="chore/i18n-auto-updates"
189+
PR_EXISTS=$(gh pr list --head "$BRANCH_NAME" --json number --jq length)
190+
if [ "$PR_EXISTS" -eq 0 ]; then
191+
echo "pr_exists=false" >> "$GITHUB_OUTPUT"
192+
else
193+
echo "pr_exists=true" >> "$GITHUB_OUTPUT"
194+
fi
195+
env:
196+
GH_TOKEN: ${{ github.token }}
197+
198+
- name: Create PR (only if doesn't exist)
199+
if: ${{ github.ref == 'refs/heads/main' && steps.changes.outputs.has_changes == 'true' && steps.check_pr.outputs.pr_exists == 'false' }}
200+
run: |
201+
gh pr create \
202+
--title "chore(i18n): auto-translate via DeepL" \
203+
--body "Automated i18n pipeline. Auto-translated strings for languages: zh_Hans, fr, es, ja, ko.
204+
This PR is automatically updated when new translatable strings are detected." \
205+
--head "chore/i18n-auto-updates" \
206+
--base "main"
207+
env:
208+
GH_TOKEN: ${{ github.token }}

.pre-commit-config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
repos:
2+
- repo: https://github.com/psf/black
3+
rev: 24.3.0
4+
hooks:
5+
- id: black
6+
language_version: python3
7+
8+
- repo: https://github.com/PyCQA/bandit
9+
rev: 1.7.6
10+
hooks:
11+
- id: bandit
12+
args: ["-r", "."]

auto-setup.sh renamed to Scripts/Auto-Setup.sh

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
#!/bin/bash
22

3-
# One-click Developer Setup Script
4-
# Author: Hamza Shahid
5-
# Description: Performs dry-run checks and launches Dockerized Django application (There is still room for improvement)
6-
73
APP_NAME="Hardhat Enterprises Web App"
84
ENV_FILE=".env"
95
ENV_SAMPLE_FILE="env.sample"
106
COMPOSE_FILE="docker-compose.yml"
117
HEALTHCHECK_URL="http://localhost:80/health"
128

13-
# Colors & symbols
149
GREEN="\033[0;32m"
1510
RED="\033[0;31m"
11+
YELLOW='\033[0;33m'
1612
NC="\033[0m"
17-
TICK="${GREEN}${NC}"
18-
CROSS="${RED}${NC}"
1913

20-
# Help
2114
show_help() {
2215
echo "Usage: $0 [OPTION]"
2316
echo
@@ -26,54 +19,54 @@ show_help() {
2619
echo " --help Show this help message"
2720
}
2821

29-
# Table headers
22+
TICK="${GREEN}Passed${NC}"
23+
CROSS="${RED}Failed${NC}"
24+
3025
show_table_header() {
31-
echo "🔧 Running checks for $APP_NAME..."
32-
echo
33-
printf "%-40s | %-10s\n" "Check" "Status"
34-
printf "%-40s-+-%-10s\n" "$(printf '%.0s-' {1..40})" "$(printf '%.0s-' {1..10})"
26+
echo -e "\n======================================================"
27+
echo -e "${YELLOW} Running checks for \"$APP_NAME\"${NC}"
28+
echo -e "======================================================\n"
3529
}
3630

37-
# Command check
3831
check_command() {
3932
if command -v "$1" >/dev/null 2>&1; then
40-
printf "%-40s | %b\n" "$2" "$TICK"
33+
echo -e "$2 : $TICK"
4134
return 0
4235
else
43-
printf "%-40s | %b\n" "$2" "$CROSS"
36+
echo -e "$2 : $CROSS"
4437
return 1
4538
fi
4639
}
4740

48-
# Docker daemon check
4941
check_docker_running() {
5042
if docker info >/dev/null 2>&1; then
51-
printf "%-40s | %b\n" "Docker daemon running" "$TICK"
43+
echo -e "Docker daemon running : $TICK"
5244
return 0
5345
else
54-
printf "%-40s | %b\n" "Docker daemon running" "$CROSS"
55-
echo -e "${RED}Docker is installed but not running. Please start Docker.${NC}"
46+
echo -e "Docker daemon running : $CROSS"
47+
echo -e "${YELLOW}Docker is installed but not running. Please start Docker.${NC}"
5648
return 1
5749
fi
5850
}
5951

60-
# File check
6152
check_file() {
62-
[ -f "$1" ] && printf "%-40s | %b\n" "$2" "$TICK" || printf "%-40s | %b\n" "$2" "$CROSS"
53+
if [ -f "$1" ]; then
54+
echo -e "$2 : $TICK"
55+
else
56+
echo -e "$2 : $CROSS"
57+
fi
6358
}
6459

65-
# Health check
6660
check_health() {
6761
if curl -s --head --request GET "$HEALTHCHECK_URL" | grep "200 OK" >/dev/null; then
68-
printf "%-40s | %b\n" "Healthcheck endpoint ($HEALTHCHECK_URL)" "$TICK"
62+
echo -e "Healthcheck endpoint ($HEALTHCHECK_URL) : $TICK"
6963
return 0
7064
else
71-
printf "%-40s | %b\n" "Healthcheck endpoint ($HEALTHCHECK_URL)" "$CROSS"
65+
echo -e "Healthcheck endpoint ($HEALTHCHECK_URL) : $CROSS"
7266
return 1
7367
fi
7468
}
7569

76-
# Perform all checks
7770
run_checks() {
7871
show_table_header
7972

@@ -91,22 +84,20 @@ run_checks() {
9184
check_health
9285
}
9386

94-
# Run setup
9587
run_setup() {
96-
echo -e "\n🔄 Starting Docker containers...\n"
88+
echo -e "\n ${YELLOW}Starting Docker containers...\n${NC}"
9789
docker-compose up --build -d
9890

99-
echo -e "\nWaiting for health endpoint..."
91+
echo -e "\n ${YELLOW}Waiting for health endpoint...${NC}"
10092
sleep 5
10193

10294
if check_health; then
103-
echo -e "\n🎉 ${GREEN}Setup complete. Visit your app at http://localhost:80/health\n"
95+
echo -e "\n${GREEN}Setup complete. Visit your app at http://localhost:80/health\n"
10496
else
105-
echo -e "\n${RED}Setup ran but app is not healthy. Please check logs using 'docker-compose logs'.${NC}\n"
97+
echo -e "\n${RED}Setup ran but app is not healthy. Please check logs using 'docker-compose logs'.${NC}\n"
10698
fi
10799
}
108100

109-
# Main
110101
case "$1" in
111102
--dry-run)
112103
run_checks

0 commit comments

Comments
 (0)