Đồng bộ hreflang cho site tĩnh 13 ngôn ngữ khi bản dịch còn viết dở: loại file chưa commit bằng git
Mình là Ayumu Kitabayashi, người phát triển ứng dụng iPhone minulo. Trang web của app (minulo.app) là site tĩnh viết tay bằng HTML, có 13 ngôn ngữ, chạy trên GitHub Pages. Không có framework, không có bước build.
Ở bài trước mình đã giới thiệu script tools/sync_langs.py: nó xem file nào thực sự có trong từng thư mục ngôn ngữ, rồi dựng lại thẻ hreflang, khối chọn ngôn ngữ ở footer và các dòng alternate trong sitemap. Bài này không nhắc lại cách script đó hoạt động. Mình viết về lỗi phát sinh khi dùng nó trong lúc nhiều đợt dịch chạy song song, và cách mình xử lý mà không phải sửa script gốc.
Lỗi xảy ra như thế nào
Mình dịch bài mới theo từng nhóm ngôn ngữ. Vài commit gần đây trên repo:
- 16 trang tiếng Ý và tiếng Indonesia
- 18 trang tiếng Việt và tiếng Nga
- 17 trang tiếng Trung phồn thể và tiếng Thái
- 40 trang tiếng Đức, Pháp, Tây Ban Nha và Bồ Đào Nha
- có một commit gộp nhiều đợt, tổng cộng 69 trang
Các đợt này không chờ nhau. Khi nhóm tiếng Ý đã xong và cần commit thì trong de/, fr/ có thể đã có file tiếng Đức, tiếng Pháp mới dịch được một nửa. Những file đó chưa git add, nghĩa là đang ở trạng thái untracked.
Trong khi đó sync_langs.py quyết định "trang này có bản ngôn ngữ nào" chỉ bằng một dòng:
have=[(h,d,n) for h,d,n in L if os.path.exists(path(d,s))]
os.path.exists không phân biệt file đã xong hay đang viết dở. Chạy sync cho slug abc.html thì file de/abc.html dở dang cũng được đưa vào hreflang và footer của tất cả các bản ngôn ngữ khác. Các file đó được commit và push, còn de/abc.html thì không. Kết quả: trên site thật có link trỏ tới trang chưa tồn tại.
Ý tưởng: dùng git để biết file nào "chưa có"
Thông tin "file nào đã xong" thật ra đã có sẵn trong git. File đã commit, hoặc file mình chủ động đưa vào danh sách của đợt này, là file đã xong. File untracked còn lại là việc đang làm dở.
Mình viết một script bọc ngoài là integ_tr.py. Nó nhận vào newfiles.txt (danh sách file mới của đợt này) và lấy danh sách untracked từ git:
untracked=subprocess.run(['git','ls-files','--others','--exclude-standard'],
capture_output=True,text=True).stdout.split()
open(J+'/th_excl.txt','w').write('\n'.join(u for u in untracked if u not in new))
th_excl.txt chứa những file cần coi như không tồn tại trong lần chạy này.
Không sửa sync_langs.py, mà tạo một bản sao đã vá
Mình muốn sync_langs.py vẫn chạy được một mình như trước, nên không thêm tham số hay chế độ mới vào nó. Thay vào đó, integ_tr.py đọc mã nguồn của nó, thay hai chỗ bằng str.replace, rồi ghi ra một file tạm sync_ex.py:
src=open('tools/sync_langs.py').read() \
.replace("ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))",
"ROOT='%s'\nEX=set(os.path.join(ROOT,l.strip()) for l in open('%s/th_excl.txt') if l.strip())"%(R,J)) \
.replace("os.path.exists(path(d,s))",
"(os.path.exists(path(d,s)) and path(d,s) not in EX)")
open(J+'/sync_ex.py','w').write(src)
Chỗ thay thứ nhất cần thiết vì bản sao nằm trong thư mục tạm, __file__ không còn trỏ về repo nên ROOT phải ghi cứng. Đồng thời nạp tập EX từ th_excl.txt. Chỗ thay thứ hai thêm điều kiện "không nằm trong EX" vào phép kiểm tra tồn tại.
Sau đó chạy bản sao cho từng slug của đợt này:
slugs=sorted(set(os.path.basename(p) for p in new))
for s in slugs: subprocess.run(['python3',J+'/sync_ex.py',s],check=True,capture_output=True)
Một điểm cần biết khi vá kiểu này: nếu sau này mình sửa đúng dòng os.path.exists(path(d,s)) trong sync_langs.py thì replace sẽ không khớp và cũng không báo lỗi. Bản sao vẫn chạy, chỉ là không loại file nào. Vì vậy mình không chỉ dựa vào bước này mà còn có bước kiểm tra ở dưới.
Kiểm tra trước khi commit: link nào cũng phải có trong git
Trước khi git add, script đi qua từng file HTML sẽ commit và kiểm tra mọi link tương đối tới .html. Đích của link phải là file đã được git theo dõi, hoặc là file mới của đợt này:
tracked=set(subprocess.run(['git','ls-files'],capture_output=True,
text=True).stdout.split())|set(new)
for h in re.findall(r'href="([^"#?:]+\.html)',x):
q=os.path.normpath(os.path.join(os.path.dirname(p),h))
if q not in tracked: print('bad link',p,h); bad+=1
Cùng vòng lặp đó, các khối <script type="application/ld+json"> được đưa qua json.loads. JSON-LD hỏng thì script dừng luôn chứ không chỉ in cảnh báo.
Bước này không quan tâm link sinh ra từ đâu (sync, hay mình tự gõ khi dịch). Chỉ cần có link tới trang chưa vào git là không commit.
Kiểm tra trùng lặp trong sitemap
locs=re.findall(r'<loc>([^<]*)</loc>',open('sitemap.xml').read())
if len(locs)!=len(set(locs)): print('DUP'); bad+=1
if bad: sys.exit('abort')
Kiểm tra này có lý do cụ thể. sync_langs.py xóa <url> cũ của slug rồi chèn lại ở cuối sitemap. Nhưng regex xóa không khớp với các mục viết theo định dạng cũ, nên những mục đó không bị xóa và URL bị lặp. Mình phát hiện ra khi đã có 23 mục trùng, và sửa ở commit d754107. Từ đó mỗi lần commit đều đếm lại <loc>. Hiện sitemap có 1.237 URL.
Chỉ add đúng những file đã kiểm tra
Danh sách file được commit gồm ba phần: file mới của đợt này, file mình sửa tay, và những file có cùng slug mà sync vừa ghi lại (lấy từ git diff --name-only):
synced=[m for m in mod if os.path.basename(m) in slugs]
files=sorted(set(new+edited+synced))
subprocess.run(['git','add']+files+['sitemap.xml','llms.txt','llms-full.txt'],check=True)
Không dùng git add -A hay git add .. Nhờ vậy các file của đợt dịch khác vẫn nằm yên ở trạng thái untracked, và lần chạy sau chúng lại bị loại khỏi hreflang cho đến khi đợt của chúng xong.
Tóm lại
- Coi git là nguồn sự thật cho câu hỏi "trang nào đã xong", còn ổ đĩa chỉ là nơi đang làm việc.
- Muốn đổi hành vi của một script nhỏ cho một trường hợp riêng thì có thể tạo bản sao đã vá lúc chạy, script gốc giữ nguyên.
- Vá bằng
replacecó thể âm thầm không khớp, nên phải có một bước kiểm tra độc lập ở cuối (link phải có trong git, JSON-LD parse được, sitemap không trùng). git addtheo danh sách file cụ thể để nhiều đợt dịch cùng tồn tại trong working tree mà không ảnh hưởng nhau.
All Rights Reserved