Skip to content

Commit 36da6fe

Browse files
authored
Merge branch 'main' into main
2 parents 3f5af3c + 23b0c9b commit 36da6fe

94 files changed

Lines changed: 21502 additions & 1850 deletions

File tree

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", "."]

Dockerfile

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,37 @@ ENV PYTHONUNBUFFERED 1
77
# Set work directory
88
WORKDIR /app
99

10-
# Install system dependencies
10+
# Install system dependencies including development tools
1111
RUN apt-get update && apt-get install -y --no-install-recommends \
1212
build-essential \
13+
curl \
14+
vim \
15+
git \
16+
postgresql-client \
1317
&& rm -rf /var/lib/apt/lists/*
1418

1519
# Install Python dependencies
1620
COPY requirements.txt .
1721
RUN pip install --upgrade pip
1822
RUN pip install --no-cache-dir -r requirements.txt
1923

20-
# Copy project
21-
COPY . .
24+
# Install additional development dependencies
25+
RUN pip install --no-cache-dir \
26+
ipython \
27+
ipdb \
28+
django-debug-toolbar \
29+
django-extensions
2230

23-
# Create necessary directories
31+
# Create necessary directories first
2432
RUN mkdir -p /app/static /app/media
2533

26-
# Run gunicorn
27-
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "core.wsgi"]
34+
# Database population is handled by Django management command in docker-compose
35+
36+
# Copy project (this will be overridden by volume mount in development)
37+
COPY . .
38+
39+
# Ensure .env file exists for SECRET_KEY persistence
40+
RUN touch /app/.env
41+
42+
# Development command - will be overridden by docker-compose
43+
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000", "--insecure"]

Dockerfile.prod

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM python:3.9
2+
3+
# set environment variables
4+
ENV PYTHONDONTWRITEBYTECODE 1
5+
ENV PYTHONUNBUFFERED 1
6+
7+
# Set work directory
8+
WORKDIR /app
9+
10+
# Install system dependencies
11+
RUN apt-get update && apt-get install -y --no-install-recommends \
12+
build-essential \
13+
&& rm -rf /var/lib/apt/lists/*
14+
15+
# Install Python dependencies
16+
COPY requirements.txt .
17+
RUN pip install --upgrade pip
18+
RUN pip install --no-cache-dir -r requirements.txt
19+
20+
# Copy project
21+
COPY . .
22+
23+
# Create necessary directories
24+
RUN mkdir -p /app/static /app/media
25+
26+
# Run gunicorn
27+
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "core.wsgi"]

0 commit comments

Comments
 (0)