Tagalog 學習 App 文章系列
將 Tagalog 學習 App 在 AWS Manila Community Day 情境中在地化為中文版本
這個 Tagalog 學習 App 最初是為 AWS Manila Community Day 製作的學習網站。接下來的工程挑戰是在地化:建立兩個中文版本,同時保留 Tagalog 的學習價值。在地化流程使用兩種相關方法。一種方法著重於透過 UI 與文字替換規則翻譯既有 HTML 網站;另一種方法則從結構化內容字典、句子卡資料、可重複使用的標籤,以及在地化語法指引重新建置在地化頁面。
免責聲明
用途:這個靜態 App 是用於語言練習與開發者分享的教育原型。它協助學習者準備 AWS Manila Community Day 可使用的禮貌 Tagalog 句子。
非商業用途:本專案沒有付費功能、沒有廣告、不需要註冊,也沒有商業目的。它是為了學習、實驗與社群活動準備而設計。
不保證正確性:產生的語言內容可能包含錯誤。Tagalog 翻譯、語法說明、發音指南與文化備註,在正式上線使用前都應由母語者檢查。
範圍:這個 App 不是 AWS 官方產品、不是官方翻譯工具,也不能取代真人語言教學。它是一個具備實用學習內容的技術 Demo。
社群尊重:這個 App 應避免刻板印象,並謹慎教導禮貌用語。像 po、opo、kayo 與 ninyo 這些字,應被說明為表達尊重的工具,而不是裝飾。
Demo
Uminom ka ng tubig dahil mainit.
語法拆解
- Uminom:意思是「喝」。
- ka:口語、非正式的「你」。
- ng:用於受詞前的標記。
- tubig:意思是「水」。
- dahil:意思是「因為」。
- mainit:意思是「熱」。
發音指南
逐字發音可以讀作: oo-mee-nohm kah ngah too-beeg dah-heel mah-mah-ee-neet.
- Uminom:oo-mee-nohm。
- tubig:too-beeg。
- dahil:dah-heel。
- mainit:mah-mah-ee-neet。 與年長者或不熟悉的人說話時,請使用含有 kayo 的禮貌版本。
內容快照
自然 Tagalog: Uminom ka ng tubig dahil mainit.
禮貌 Tagalog: Uminom po kayo ng tubig dahil mainit.
額外例句 1: Uminom ka muna ng malamig na tubig.
額外例句 2: Magdala ka ng tubig dahil mainit sa labas.
相關安全例句: Ilapit mo ang bag mo sa iyo.
目錄
第 1 部分:將 UI 文字與學習內容分開在地化
本節說明為什麼在地化時應將介面文字、文章中繼資料、學習筆記與句子卡內容分開處理。UI 標籤經常變動,但 Tagalog 學習句子應保持穩定,方便發音練習。
第 2 部分:將語言設定視為產品契約
本節展示語言字典如何定義 ZIP 名稱、輸出資料夾、HTML 語言標記、網站品牌名稱、導覽標籤、文章列表按鈕、備註、分類與文化指引。
第 3 部分:使用替換對照表在地化既有 HTML
本節說明第一條在地化路線:讀取既有 HTML 檔案、替換介面字串與文章標題、註入在地化備註,然後將結果封裝成各語言版本的 ZIP 檔。
第 4 部分:從結構化內容產生在地化網站
本節說明第二條在地化路線:直接從文章中繼資料、句子卡陣列、語法輔助函式、例句、CSS、JavaScript 與範本產生在地化網站。
第 1 部分:將 UI 文字與學習內容分開在地化
目標
在不破壞 Tagalog 學習體驗的前提下在地化 App。App 應將介面與說明翻譯成中文版本,但 Tagalog 句子必須可見且保持穩定,讓學習者能練習口說。
開發技能
The development skill is separation of concerns. The project treats these layers differently:
UI layer:
- site title
- article list label
- open menu button
- start button
- category badges
- practice notes
內容層:
- article titles
- article summaries
- sentence-card labels
- Chinese explanations
- grammar notes
- localized examples
語言學習層:
- Natural Tagalog
- Polite Tagalog
- Friendly Filipino-English
- Playful Filipino-English
Tagalog 層不應被盲目翻譯掉,因為學習者需要閱讀並練習發音。
Prompt
建立兩個靜態 Tagalog 學習網站的在地化版本:
- Cantonese / Hong Kong Chinese
- Taiwan Chinese / Traditional Chinese
翻譯 UI、文章標題、摘要、學習筆記、語法標籤、文化說明與例句意思。
保留 Tagalog 句子文字,方便發音練習。
每個語言版本輸出一個 ZIP 檔。
結果
輸出結果會成為兩個獨立的靜態網站。每個網站都有在地化導覽與說明,同時 Tagalog 句子仍可用於口說練習。
提示
- 如果 Tagalog 句子本身是學習目標,就不要翻譯它。
- 翻譯句子周圍的標籤與說明。
- 為 UI 與內容使用分開的字典。
- Use
html langmarkers such aszh-HKandzh-TW. - ZIP 名稱要明確,讓審閱者知道自己正在開啟哪個語言版本。
第 2 部分:將語言設定視為產品契約
目標
透過設定物件描述每個語言版本,讓每個版本的輸出都可預測。
開發技能
關鍵技能是設定驅動的在地化。語言設定包含輸出 ZIP 檔、資料夾名稱、HTML 語言標記、品牌文字、導覽文字、文章列表控製項、學習備註、分類名稱與文化指引。
範例設定模式
lang_cfgs = {
"zh_tw": {
"zip": "tagalog_static_html_site_taiwan_chinese_zh_tw.zip",
"folder": "tagalog_zh_tw",
"html_lang": "zh-TW",
"site": "AWS 馬尼拉社群日 Tagalog 學習站",
"list": "文章列表",
"open": "開啟文章列表",
"choose": "選擇文章",
"note": "繁體中文(台灣)版本:介面、說明、語法提示與學習語境為繁體中文;Tagalog 句子保留原文方便朗讀練習。"
}
}
為什麼這很重要
語言設定會成為產品契約。它告訴產生器要把檔案寫到哪裡、頁面要如何標示、體驗要如何描述,以及結果要如何封裝。沒有這個契約,每個在地化頁面都可能逐漸偏離一致性。
Prompt
為每個中文變體建立語言設定。
每個語言設定都必須包含:
- zip file name
- output folder
- html lang code
- site title
- article list label
- open article list button
- start button
- language note
- category labels
- sentence-card labels
- respect guidance
- casual usage guidance
結果
產生器可以逐一讀取語言設定,並用相同結構建立每個在地化網站。
提示
- 將語言相關文字集中存放在同一處。
- 避免在範本中硬編碼標籤。
- 保持資料夾名稱與 ZIP 名稱一致。
- Use language keys consistently:
zh_hk,zh_tworzh-hk,zh-tw. - 讓每個使用者可見字串都可以被搜尋。
第 3 部分:使用替換對照表在地化既有 HTML
目標
取得已正確產生的 HTML 網站,並在不從零重建所有頁面的情況下進行在地化。
開發技能
This is the replacement-map approach. The workflow is:
1. Find source HTML files.
2. 定義語言字典。
3. 定義文章標題對照表。
4. 定義摘要對照表。
5. 定義共用 UI 替換對照表。
6. 在每個 HTML 檔案中替換字串。
7. 註入在地化備註。
8. 將檔案寫入語言專屬資料夾。
9. 將每個資料夾封裝為 ZIP 檔。
10. 執行一致性檢查。
當既有 HTML 結構已正確,而目標是快速修補文字時,這個方法很有用。
Prompt
使用既有靜態 HTML 頁面作為來源檔。
針對每個語言版本:
- replace site title and navigation labels
- replace article titles and summaries
- replace sentence-card labels
- replace cultural guidance text
- add a localized note in the hero section
- set the HTML lang attribute
- package the localized files as a ZIP
替換分類範例
article_title_map:
- Community Day: Greetings and Respectful Introductions
- Community Day: Directions, Venue, and Registration
- Friendship Tagalog: Honest Heart and Deep Feelings
summary_map:
- 原始英文文章摘要
- 在地化中文摘要
common_replacements:
- Background context
- English Input
- Natural Tagalog
- Polite Tagalog
- Tone
- Cultural Context
- Pronunciation Guide
結果
在地化工具可以從既有頁面建立語言版本,而不需要重寫整套產生邏輯。
提示
- 替換對照表很快,但如果來源文字改變就會變得脆弱。
- 必須仔細比對完全相同的字串。
- 在地化後搜尋是否仍有殘留英文標籤。
- 註入新內容時使用 HTML escape。
- 這個方法適合 UI 修補,不適合複雜內容重組。
附件實作範例:完整在地化管線
The updated attachment adds a concrete batch-processing pattern from lang_ui.py. This example fits the replacement-map route because it iterates through language configs, rebuilds each localized folder, reads every source HTML file, applies localization, and writes the localized page back to the language-specific output folder.
for lang, cfg in langs.items():
folder = Path(cfg['folder'])
if folder.exists():
shutil.rmtree(folder)
folder.mkdir()
for src in source_files:
text = src.read_text(encoding='utf-8')
localized = localize(text, lang, cfg)
(folder / src.name).write_text(localized, encoding='utf-8')
附件實作範例:HTML lang 替換
The update also adds a regex-based example for replacing the HTML language marker. This belongs in the existing-HTML localization flow because it patches generated pages without rebuilding the full template.
text = re.sub(r'<html lang="en">', f'<html lang="{cfg["html_lang"]}">', text)
附件實作範例:將在地化備註注入 HTML
The localized note can be injected into the first matching section so reviewers immediately understand which Chinese variant they are viewing and why Tagalog text is preserved.
note_html = f"<p class='notice'>{cfg['note']}</p>"
text = text.replace('</section>', note_html + '</section>', 1)
UI 在地化
The localization section because it localizes an all right-generated static HTML site. It reads index.html and article-*.html, defines Chinese-variant UI dictionaries, maps English article titles and summaries to localized versions, replaces common sentence-card labels, injects localized learning notes, writes per-language folders, packages each folder into a ZIP file, and prints secondary validation checks.
from pathlib import Path
import zipfile, re, json, shutil, os
source_files = [Path('index.html')] + sorted(Path('.').glob('article-*.html'))
source_files = [p for p in source_files if p.exists()]
assert source_files, 'No source HTML files found.'
langs = {
'zh-hk': {
'zip': 'tagalog_static_html_site_zh_hk_cantonese.zip',
'folder': 'tagalog_static_html_site_zh_hk',
'html_lang': 'zh-HK',
'brand': 'AWS 馬尼拉 Community Day Tagalog 學習站',
'pill': '深色模式 · 手機自適應 · 10 篇文章',
'static_pill': '深色模式 · 靜態 HTML',
'articles_list': '文章清單',
'choose': '揀一篇文章',
'open': '打開文章清單',
'start': '開始第 1 篇',
'start_summary': '打開第一課,開始練習去馬尼拉時用得着嘅禮貌問候語。',
'intro': '呢個靜態學習網站有 10 個文章頁,每篇都有 40 張句子卡,用嚟練 Tagalog、文化語境、禮貌講法同有趣 Filipino-English。',
'note': '呢個廣東話版本已翻譯介面、文章說明同學習提示;Tagalog 句子保留原文,方便你直接朗讀練習。',
'category_community': 'Community Day',
'category_friendship': '浪漫句子',
'respect': '尊重:',
'practice': '練習:',
'culture': '文化:',
'respect_text': '同長輩、講者、主辦、義工、場地職員,或者第一次見面嘅人講嘢時,可以用 po、opo、kayo 同 ninyo 表示尊重。',
'practice_text': '先讀禮貌版本,再比較自然版本同 playful 版本。',
'culture_text': '細細個字都可以表達溫暖、謙遜、感謝、耐性同歸屬感。',
},
'zh-tw': {
'zip': 'tagalog_static_html_site_zh_tw_taiwan_chinese.zip',
'folder': 'tagalog_static_html_site_zh_tw',
'html_lang': 'zh-TW',
'brand': 'AWS 馬尼拉社群日 Tagalog 學習站',
'pill': '深色模式 · 行動裝置響應式 · 10 篇文章',
'static_pill': '深色模式 · 靜態 HTML',
'articles_list': '文章列表',
'choose': '選擇一篇文章',
'open': '開啟文章列表',
'start': '開始第 1 篇',
'start_summary': '開啟第一課,開始練習前往馬尼拉時能使用的禮貌問候語。',
'intro': '這個靜態學習網站包含 10 個文章頁面,每篇都有 40 張句子卡,用來練習 Tagalog、文化情境、禮貌表達與有趣的 Filipino-English。',
'note': '本繁體中文(台灣)版本已翻譯介面、文章說明與學習提示;Tagalog 句子保留原文,方便直接朗讀練習。',
'category_community': '社群日',
'category_friendship': '浪漫句子',
'respect': '尊重:',
'practice': '練習:',
'culture': '文化:',
'respect_text': '面對長輩、講者、主辦單位、志工、場地人員與第一次見面的人時,使用 po、opo、kayo 與 ninyo 表示尊重。',
'practice_text': '先朗讀禮貌版本,再比較自然版本與 playful 版本。',
'culture_text': '小小的詞也能傳達溫暖、謙遜、感謝、耐心與歸屬感。',
}
}
article_title_map = {
'Community Day: Greetings and Respectful Introductions': {
'zh-hk':'Community Day:問候同禮貌自我介紹','zh-tw':'社群日:問候與禮貌自我介紹'},
'Community Day: Directions, Venue, and Registration': {
'zh-hk':'Community Day:方向、場地同登記','zh-tw':'社群日:方向、會場與報到'},
'Community Day: Workshop Questions and Hands-On Learning': {
'zh-hk':'Community Day:Workshop 提問同實作學習','zh-tw':'社群日:工作坊提問與實作學習'},
'Community Day: Networking, Connect, and Belong': {
'zh-hk':'Community Day:Networking、連結同歸屬感','zh-tw':'社群日:交流、連結與歸屬感'},
'Community Day: Volunteers, Waiting, Time, and Goodbye': {
'zh-hk':'Community Day:義工、等候、時間同道別','zh-tw':'社群日:志工、等待、時間與道別'},
'Friendship Tagalog: One Question, One Gentle Answer': {
'zh-hk':'浪漫 Tagalog:一個問題,一個溫柔答案','zh-tw':'浪漫 Tagalog:一個問題,一個溫柔答案'},
'Friendship Tagalog: Friends Only Feelings': {
'zh-hk':'浪漫 Tagalog:原來只係朋友嘅感覺','zh-tw':'浪漫 Tagalog:只是朋友的心情'},
'Friendship Tagalog: You Are My Beloved': {
'zh-hk':'浪漫 Tagalog:你係我珍惜嘅人','zh-tw':'浪漫 Tagalog:你是我珍愛的人'},
'Friendship Tagalog: Honest Heart and Deep Feelings': {
'zh-hk':'浪漫 Tagalog:真心同深層感受','zh-tw':'浪漫 Tagalog:真誠的心與深層感受'},
'Friendship Tagalog: Distance, Hope, and Promises': {
'zh-hk':'浪漫 Tagalog:距離、希望同承諾','zh-tw':'浪漫 Tagalog:距離、希望與承諾'},
}
summary_map = {
'Practice polite greetings, names, beginner phrases, and warm introductions for meeting speakers, volunteers, students, and builders in Manila confidently today.': {
'zh-hk':'練習禮貌問候、姓名介紹、初學短句同自然開場,幫你喺馬尼拉自信認識講者、義工、學生同 builders。',
'zh-tw':'練習禮貌問候、姓名介紹、初學短句與溫暖開場,協助你在馬尼拉自信認識講者、志工、學生與 builders。'},
'Learn useful venue phrases for registration, rooms, seats, restrooms, exits, queues, badges, and polite help requests during the event today.': {
'zh-hk':'學習場地實用句,涵蓋登記、房間、座位、洗手間、出口、排隊、名牌同禮貌求助。',
'zh-tw':'學習會場實用短句,涵蓋報到、房間、座位、洗手間、出口、排隊、名牌與禮貌求助。'},
'Ask workshop questions, request repeats, discuss demos, serverless ideas, AI solutions, cloud labs, and learning goals with respectful Tagalog practice.': {
'zh-hk':'用禮貌 Tagalog 練習 workshop 提問、請人重複、討論 demo、serverless 諗法、AI 方案、cloud lab 同學習目標。',
'zh-tw':'用禮貌 Tagalog 練習工作坊提問、請對方重複、討論 demo、serverless 想法、AI 解決方案、雲端實驗與學習目標。'},
'Use friendly phrases to connect with developers, students, leaders, and peers while expressing excitement, humility, gratitude, belonging, and respect all right.': {
'zh-hk':'用友善句子連結 developers、學生、leaders 同 peers,同時表達興奮、謙遜、感謝、歸屬感同尊重。',
'zh-tw':'使用友善短句連結開發者、學生、領導者與夥伴,同時表達興奮、謙遜、感謝、歸屬感與尊重。'},
'Thank volunteers, manage waiting time, apologize politely, coordinate arrivals, say goodbye warmly, and respect schedules with practical event phrases today.': {
'zh-hk':'用實用活動句感謝義工、處理等候時間、禮貌道歉、協調到達、溫暖道別,同尊重時間表。',
'zh-tw':'用實用活動短句感謝志工、管理等待時間、禮貌道歉、協調抵達、溫暖道別,並尊重時程安排。'},
}
common_replacements = {
'Background context:': {'zh-hk':'背景情境:','zh-tw':'背景情境:'},
'English Input:': {'zh-hk':'英文輸入:','zh-tw':'英文輸入:'},
'自然 Tagalog:': {'zh-hk':'自然 Tagalog:','zh-tw':'自然 Tagalog:'},
'禮貌 Tagalog:': {'zh-hk':'禮貌 Tagalog:','zh-tw':'禮貌 Tagalog:'},
'Friendly Filipino-English:': {'zh-hk':'友善 Filipino-English:','zh-tw':'友善 Filipino-English:'},
'有趣 Filipino-English:': {'zh-hk':'玩味 Filipino-English:','zh-tw':'有趣 Filipino-English:'},
'Tone:': {'zh-hk':'語氣:','zh-tw':'語氣:'},
'Cultural Context:': {'zh-hk':'文化語境:','zh-tw':'文化情境:'},
'Grammatical Breakdown:': {'zh-hk':'語法拆解:','zh-tw':'語法拆解:'},
'Common Context and Idiomatic Use:': {'zh-hk':'常見語境同慣用法:','zh-tw':'常見情境與慣用法:'},
'Extra Examples:': {'zh-hk':'基本例子:','zh-tw':'基礎例子:'},
'Pronunciation Guide:': {'zh-hk':'發音指南:','zh-tw':'發音指南:'},
'Related understand emotional, poetic, polite, and practical sentence patterns:': {'zh-hk':'相關:理解情感、詩意、禮貌同實用句型:','zh-tw':'相關:理解情感、詩意、禮貌與實用句型:'},
'Sentence ': {'zh-hk':'句子 ','zh-tw':'句子 '},
'Article ': {'zh-hk':'第 ','zh-tw':'第 '},
' of 24': {'zh-hk':' / 10 篇','zh-tw':' / 10 篇'},
'40 sentence cards': {'zh-hk':'40 張句子卡','zh-tw':'40 張句子卡'},
'Each card separates 自然 Tagalog, respectful Tagalog, 友善 Filipino-English, and 有趣 Filipino-English so learners can choose the right tone.': {
'zh-hk':'每張卡會分開顯示自然 Tagalog、禮貌 Tagalog、友善 Filipino-English 同玩味 Filipino-English,方便學習者揀啱語氣。',
'zh-tw':'每張卡都會分開顯示自然 Tagalog、禮貌 Tagalog、友善 Filipino-English 與有趣 Filipino-English,方便學習者選擇合適語氣。'},
'Use the polite form with elders, speakers, organizers, volunteers, venue staff, and first-time contacts. Use casual speech with friends or peers when the situation feels relaxed.': {
'zh-hk':'同長輩、講者、主辦、義工、場地職員同第一次見面嘅人講嘢時,用禮貌版本。同朋友或熟咗嘅 peers 傾偈時,可以用輕鬆講法。',
'zh-tw':'面對長輩、講者、主辦單位、志工、場地人員與第一次見面的人時,使用禮貌版本。和朋友或熟悉夥伴聊天時,可使用較輕鬆說法。'},
'Let us rest in the shade first.': {'zh-hk':'多謝你幫忙。','zh-tw':'謝謝你的幫忙。'},
'Sit for a moment if you feel dizzy.': {'zh-hk':'我可唔可以問一個問題?','zh-tw':'我可以問一個問題嗎?'},
'Please check if the payment came in.': {'zh-hk':'我已經到咗。','zh-tw':'我已經到了。'},
'Choose an Article': {'zh-hk':'揀一篇文章','zh-tw':'選擇一篇文章'},
'Open Articles List': {'zh-hk':'打開文章清單','zh-tw':'開啟文章列表'},
'Start Article 1': {'zh-hk':'開始第 1 篇','zh-tw':'開始第 1 篇'},
'Open the first lesson and begin practicing respectful greetings for Manila.': {'zh-hk':'打開第一課,開始練習去馬尼拉時用得着嘅禮貌問候語。','zh-tw':'開啟第一課,開始練習前往馬尼拉時能使用的禮貌問候語。'},
}
def localize(text, lang, cfg):
# html lang
text = re.sub(r'<html lang="en"|<html lang=\'en\'', lambda m: m.group(0).replace('en', cfg['html_lang']), text)
# common fixed strings
text = text.replace('Tagalog | AWS Community Day', cfg['brand'])
text = text.replace('Dark mode · Mobile responsive · 24 articles', cfg['pill'])
text = text.replace('Dark mode · Static HTML', cfg['static_pill'])
text = text.replace('Articles List', cfg['articles_list'])
text = text.replace('Community Day', cfg['category_community'])
text = text.replace('Friendship', cfg['category_friendship'])
# article titles
for en, m in article_title_map.items():
text = text.replace(en, m[lang])
for en, m in summary_map.items():
text = text.replace(en, m[lang])
for en, m in common_replacements.items():
text = text.replace(en, m[lang])
# Replacements for info boxes
text = re.sub(r'<strong>Respect:</strong><br>Use po, opo, kayo, and ninyo for elders, speakers, organizers, volunteers, venue staff, and first-time contacts\.', f'<strong>{cfg["respect"]}</strong><br>{cfg["respect_text"]}', text)
text = re.sub(r'<strong>Practice:</strong><br/?>Read the natural version first, then the polite version, then try the Filipino-English bridge\.', f'<strong>{cfg["practice"]}</strong><br>{cfg["practice_text"]}', text)
text = re.sub(r'<strong>Culture:</strong><br>Small words can carry warmth, humility, gratitude, patience, and a feeling of belonging\.', f'<strong>{cfg["culture"]}</strong><br>{cfg["culture_text"]}', text)
# Landing intro
text = text.replace('This static learning site includes 24 article pages, each with 40 sentence cards for Tagalog practice, cultural context, respectful speech, and 有趣 Filipino-English.', cfg['intro'])
# Add localization note in hero before closing first hero section if article page
note_html = f'<p class="notice">{cfg["note"]}</p>'
if cfg['note'] not in text:
text = text.replace('</section>\n <button class="mobile-menu-button"', note_html + '\n </section>\n <button class="mobile-menu-button"', 1)
text = text.replace('</section><button class=\'mobile-menu-button\'', note_html + '</section><button class=\'mobile-menu-button\'', 1)
# Meta description charset title handled via title replacements
return text
created = []
for lang, cfg in langs.items():
folder = Path(cfg['folder'])
if folder.exists():
shutil.rmtree(folder)
folder.mkdir()
for src in source_files:
text = src.read_text(encoding='utf-8')
localized = localize(text, lang, cfg)
(folder / src.name).write_text(localized, encoding='utf-8')
zip_path = Path(cfg['zip'])
if zip_path.exists():
zip_path.unlink()
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
for p in sorted(folder.glob('*.html'), key=lambda x: (x.name != 'index.html', x.name)):
z.write(p, arcname=f"{folder.name}/{p.name}")
created.append(str(zip_path))
# Secondary consistency checks
checks = {}
for lang, cfg in langs.items():
folder = Path(cfg['folder'])
html_files = list(folder.glob('*.html'))
checks[lang] = {
'zip_exists': Path(cfg['zip']).exists(),
'html_count': len(html_files),
'article_pages': len(list(folder.glob('article-*.html'))),
'index_exists': (folder/'index.html').exists(),
'lang_marker_count': sum(cfg['html_lang'] in p.read_text(encoding='utf-8') for p in html_files),
'mobile_css_pages': sum('@media (max-width: 860px)' in p.read_text(encoding='utf-8') for p in html_files),
'sentence_card_counts': {p.name: p.read_text(encoding='utf-8').count('class="sentence-card"') for p in folder.glob('article-*.html')},
'zip_size': Path(cfg['zip']).stat().st_size,
}
print(json.dumps({'created': created, 'checks': checks}, ensure_ascii=False, indent=2))
第 4 部分:從結構化內容產生在地化網站
目標
直接從結構化資料建立在地化頁面,而不是只在 HTML 中替換字串。
開發技能
這是結構化產生方法。產生器不是從既有 HTML 開始,而是從在地化文章中繼資料、句子卡資料、語法輔助函式、例句、CSS 與範本開始。
資料模型
lang_cfgs:
- 語言層級的 UI 與標籤契約
articles_base:
- slug
- category
- 各語言的在地化標題與摘要
comm_cards:
- 中文意思
- 自然 Tagalog
- 禮貌 Tagalog
- 友善 Filipino-English
- 有趣 Filipino-English
- tone
friendship_cards:
- 中文意思
- 自然 Tagalog
- 禮貌 Tagalog
- 友善 Filipino-English
- 有趣 Filipino-English
- tone
Prompt
從結構化資料產生在地化靜態 HTML 頁面。
For each language:
- create index.html
- create 24 article pages
- each article page must have 40 sentence cards
- use localized article titles and summaries
- use localized sentence-card labels
- preserve Tagalog text
- package the output as a ZIP
句子卡資料範例
中文意思: 我可以問一個問题嗎?
自然 Tagalog: Umupo ka sandali kung nahihilo ka.
禮貌 Tagalog: Umupo po kayo sandali kung nahihilo kayo.
Friendly Filipino-English: Can I ask po?
Playful Filipino-English: Magtatanong na ako, all right.
Tone: 禮貌提問、工作坊
結果
The generated localized site becomes more stable than raw replacement because the source data is all right multilingual. If an article title needs to change, the developer updates articles_base. If a label needs to change, the developer updates lang_cfgs. If a phrase needs improvement, the developer updates comm_cards or friendship_cards.
提示
- 長期維護時,優先使用結構化產生。
- 替換對照表只適合快速修補。
- 將在地化標題與摘要資料放在文章中繼資料附近。
- 所有語言共用同一個卡片 renderer。
- 所有語言版本重複使用相同的 CSS 與 JavaScript。
本節說明第二條在地化路線:直接從文章中繼資料、句子卡陣列、語法輔助函式、例句、CSS、JavaScript 與範本產生在地化網站。
結構化產生器迴圈
Adds a structured generation loop from lang_content.py. This fits the long-term generation route because each article page is produced from article metadata, language config, and a reusable article_page renderer.
for i,(slug,cat,trans) in enumerate(articles_base, start=1):
(folder/f'article-{i}.html').write_text(
article_page(i,slug,cat,trans,lang_key,cfg),
encoding='utf-8'
)
附件實作範例:卡片渲染模式
The same update includes the sentence-card rendering loop. It demonstrates how each generated article can consistently produce 40 practice cards while reusing base sentence data.
for n in range(1,41):
cards.append(card_html(n, bases[(n-1)%len(bases)]))
結構化在地化網站產生器
The structured-content generation section because it rebuilds localized pages from language configuration, localized article metadata, reusable sentence-card arrays, grammar helper functions, examples, CSS, templates, ZIP 封裝, and validation checks. This complements lang_ui.py: lang_ui.py patches existing HTML, while lang_content.py generates localized HTML from structured multilingual data.
from pathlib import Path
import zipfile, shutil, json, html, re
lang_cfgs = {
'zh_hk': {
'zip':'tagalog_static_html_site_cantonese_zh_hk.zip','folder':'tagalog_zh_hk','html_lang':'zh-HK',
'site':'AWS 馬尼拉 Community Day Tagalog 學習站','list':'文章清單','open':'打開文章清單','choose':'揀文章','start':'開始學習','note':'廣東話版本:介面、說明、語法提示同學習語境用廣東話;Tagalog 句子保留原文,方便直接朗讀練習。',
'labels':['背景情境','中文意思','自然 Tagalog','禮貌 Tagalog','友善 Filipino-English','玩味 Filipino-English','語氣','文化語境','語法拆解','常見語境同用法','基本例子','發音指南','相關句型'],
'cat_comm':'Community Day','cat_friendship':'浪漫句子','sentence':'句子','article':'第 {n} / 10 篇','cards':'40 張句子卡',
'respect':'同講者、主辦、義工、長輩或者第一次見面嘅人講嘢時,優先用 po、opo、kayo、ninyo 表示尊重。',
'casual':'同熟朋友或者 peers 傾偈時,可以用自然版本或者玩味版本。',
},
'zh_tw': {
'zip':'tagalog_static_html_site_taiwan_chinese_zh_tw.zip','folder':'tagalog_zh_tw','html_lang':'zh-TW',
'site':'AWS 馬尼拉社群日 Tagalog 學習站','list':'文章列表','open':'開啟文章列表','choose':'選擇文章','start':'開始學習','note':'繁體中文(台灣)版本:介面、說明、語法提示與學習情境為繁體中文;Tagalog 句子保留原文,方便朗讀練習。',
'labels':['背景情境','中文意思','自然 Tagalog','禮貌 Tagalog','友善 Filipino-English','有趣 Filipino-English','語氣','文化情境','語法拆解','常見情境與用法','基礎例子','發音指南','相關句型'],
'cat_comm':'社群日','cat_friendship':'浪漫句子','sentence':'句子','article':'第 {n} / 10 篇','cards':'40 張句子卡',
'respect':'面對講者、主辦單位、志工、長輩或第一次見面的人時,優先使用 po、opo、kayo、ninyo 表示尊重。',
'casual':'與熟悉朋友或夥伴交流時,可以使用自然版本或有趣版本。',
}
}
# Titles and summaries per language
articles_base = [
('community-greetings-introductions','comm',{
'zh_hk':('Community Day:問候同禮貌自我介紹','練習禮貌問候、姓名介紹、初學短句同自然開場,幫你喺馬尼拉自信認識講者、義工、學生同 builders。'),
'zh_tw':('社群日:問候與禮貌自我介紹','練習禮貌問候、姓名介紹、初學短句與溫暖開場,協助你在馬尼拉自信認識講者、志工、學生與 builders。')}),
('community-directions-registration','comm',{
'zh_hk':('Community Day:方向、場地同登記','學習場地實用句,涵蓋登記、房間、座位、洗手間、出口、排隊、名牌同禮貌求助。'),
'zh_tw':('社群日:方向、會場與報到','學習會場實用短句,涵蓋報到、房間、座位、洗手間、出口、排隊、名牌與禮貌求助。')}),
('community-workshops-learning','comm',{
'zh_hk':('Community Day:Workshop 提問同實作學習','用禮貌 Tagalog 練習 workshop 提問、請人重複、討論 demo、serverless 諗法、AI 方案、cloud lab 同學習目標。'),
'zh_tw':('社群日:工作坊提問與實作學習','用禮貌 Tagalog 練習工作坊提問、請對方重複、討論 demo、serverless 想法、AI 解決方案、雲端實驗與學習目標。')}),
('community-networking-belonging','comm',{
'zh_hk':('Community Day:Networking、連結同歸屬感','用友善句子連結 developers、學生、leaders 同 peers,同時表達興奮、謙遜、感謝、歸屬感同尊重。'),
'zh_tw':('社群日:交流、連結與歸屬感','使用友善短句連結開發者、學生、領導者與夥伴,同時表達興奮、謙遜、感謝、歸屬感與尊重。')}),
('community-volunteers-time-goodbye','comm',{
'zh_hk':('Community Day:義工、等候、時間同道別','用實用活動句感謝義工、處理等候時間、禮貌道歉、協調到達、溫暖道別,同尊重時間表。'),
'zh_tw':('社群日:志工、等待、時間與道別','用實用活動短句感謝志工、管理等待時間、禮貌道歉、協調抵達、溫暖道別,並尊重時程安排。')}),
('friendship-one-question-one-answer','rom',{
'zh_hk':('浪漫 Tagalog:一個問題,一個溫柔答案','練習關於真誠、答案、希望、勇氣同真實感受嘅原創浪漫問題,同時避免複製受版權保護歌詞。'),
'zh_tw':('浪漫 Tagalog:一個問題,一個溫柔答案','練習關於真誠、答案、希望、勇氣與真實感受的原創浪漫問題,同時避免複製受著作權保護的歌詞。')}),
('friendship-friends-only-feelings','rom',{
'zh_hk':('浪漫 Tagalog:原來只係朋友嘅感覺','學習關於友情、不確定、希望、界線同情感清晰度嘅溫柔句,用尊重、柔和、初學者友善嘅 Tagalog 句型。'),
'zh_tw':('浪漫 Tagalog:只是朋友的心情','學習關於友情、不確定、希望、界線與情感清晰度的溫柔短句,使用尊重、柔和、適合初學者的 Tagalog 句型。')}),
('friendship-you-are-my-beloved','rom',{
'zh_hk':('浪漫 Tagalog:你係我珍惜嘅人','探索關於關心、情感、信任、支持同選擇某人嘅原創詩意句,用溫暖但簡單嘅 Tagalog 表達。'),
'zh_tw':('浪漫 Tagalog:你是我珍愛的人','探索關於關心、情感、信任、支持與選擇某人的原創詩意短句,用溫暖但簡單的 Tagalog 表達。')}),
('friendship-honest-heart-deep-feelings','rom',{
'zh_hk':('浪漫 Tagalog:真心同深層感受','建立關於真實情緒、緊張嘅心、真誠欣賞、安靜掛念同小心告白嘅自然禮貌 Tagalog 詞彙。'),
'zh_tw':('浪漫 Tagalog:真誠的心與深層感受','建立關於真實情緒、緊張的心、真誠欣賞、安靜思念與謹慎告白的自然禮貌 Tagalog 詞彙。')}),
('friendship-distance-hope-promises','rom',{
'zh_hk':('浪漫 Tagalog:距離、希望同承諾','練習關於距離、等候、承諾、南北、希望、耐性同穩定情感嘅原創句。'),
'zh_tw':('浪漫 Tagalog:距離、希望與承諾','練習關於距離、等待、承諾、南北、希望、耐心與穩定情感的原創短句。')}),
]
comm_cards = [
('謝謝你的幫助。','Magpahinga muna tayo sa lilim.','Magpahinga muna po tayo sa lilim.','Rest muna tayo sa shade po.','Shade break muna tayo, all right.','health, safety, daily practice'),
('我可以問一個問题嗎?','Umupo ka sandali kung nahihilo ka.','Umupo po kayo sandali kung nahihilo kayo.','Sit muna po kung dizzy.','Umupo muna, easy lang all right.','health advice, care'),
('我已經到了。','Paki-check kung pumasok ang bayad.','Paki-check po kung pumasok ang bayad.','Please check po kung pumasok ang payment.','Payment check muna, all right.','payment, polite request'),
('請慢一點。','Pakibagalan.','Pakibagalan po.','Slow down po please.','Slow lang po, catching up all right.','學習支持、禮貌'),
('我正在學習 Tagalog。','Nag-aaral ako ng Tagalog.','Nag-aaral po ako ng Tagalog.','Learning Tagalog po ako.','Learning Tagalog na ako, all right.','自我介紹、友善'),
('我很興奮參加社區日。','Excited akong sumali sa Community Day.','Excited po akong sumali sa Community Day.','Excited po ako for Community Day.','Excited na ako, all right.','興奮、歸屬感'),
('洗手間在哪里?','Saan ang restroom?','Saan po ang restroom?','Saan po ang CR?','Saan ang CR, quick lang all right.','方向、實用'),
('下一場什么時候開始?','Anong oras magsisimula ang susunod na session?','Anong oras po magsisimula ang susunod na session?','What time po next session?','Next session what time, all right.','時間、活动安排'),
('請等我几分鐘。','Hintayin mo ako ng ilang minuto.','Hintayin n’yo po ako ng ilang minuto.','Wait me for a few minutes po.','Hintayin mo ako, papunta na all right.','等待、协調'),
('再見,下次見。','Kita tayo sa susunod.','Kita po tayo sa susunod.','See you po next time.','Kita-kits next time, all right.','道别、溫暖'),
]
friendship_cards = [
('Thank you for being my safe friend.','Salamat sa pagiging ligtas kong kaibigan.','Salamat sa pagiging ligtas ko pong kaibigan.','Thank you po for being my safe friend.','Safe friend ka, thank you all right.','gentle friendship support'),
('我希望你理解我。','Sana maintindihan mo ako.','Sana po maintindihan ninyo ako.','Hope po you understand me.','Sana gets mo ako, all right.','希望、柔和'),
('我會帶着尊重倾聽。','Makikinig ako nang may respeto.','Makikinig po ako nang may respeto.','Listen po ako with respect.','Makikinig ako, respect mode all right.','成熟、尊重'),
('我的感受是真誠的。','Tapat ang damdamin ko.','Tapat po ang damdamin ko.','Sincere po feelings ko.','Tapat ang damdamin ko, all right.','情感、诗意'),
('我不會強迫你的感受。','Hindi ko pipilitin ang damdamin mo.','Hindi ko po pipilitin ang damdamin ninyo.','No pressure po sa feelings.','No forcing feelings, respectful all right.','界線、尊重'),
('我的心正在學習勇敢。','Natututong maging matapang ang puso ko.','Natututo pong maging matapang ang puso ko.','Heart ko learning bravery po.','Heart bravery training all right.','脆弱、希望'),
('不管距离多遠,我仍然關心。','Kahit malayo, nagmamalasakit ako.','Kahit malayo po, nagmamalasakit po ako.','Even far po, I care.','Malayo man, care pa rin all right.','距离、關心'),
('請溫柔地接受我的話。','Tanggapin mo sana nang malumanay ang mga salita ko.','Tanggapin po sana ninyo nang malumanay ang mga salita ko.','Receive po my words gently.','Words ko gentle delivery all right.','溫柔、告白'),
('我會耐心等待。','Maghihintay ako nang may pasensya.','Maghihintay po ako nang may pasensya.','Wait po ako with patience.','Waiting patiently all right.','等待、耐心'),
('再見,願你平安。','Paalam, sana payapa ka.','Paalam po, sana po ay payapa kayo.','Goodbye po, peace sana.','Paalam, peaceful ending all right.','道别、诗意'),
]
# Per language localized grammar words
def grammar_items(card, cfg_key):
cn = {
'zh_hk': [('po','禮貌標記,常用嚟表示尊重。'),('kayo / ninyo','禮貌或眾數嘅「你 / 你哋 / 您的」。'),('na / all right','表示「已經 / 而家」,玩味版本會加 all right 做輕鬆語氣。')],
'zh_tw': [('po','禮貌標記,常用來表示尊重。'),('kayo / ninyo','禮貌或複數的「你 / 你們 / 您的」。'),('na / all right','表示「已經 / 現在」,有趣版本會加 all right 作輕鬆語氣。')],
}
return cn[cfg_key]
def examples(is_comm, cfg_key):
if is_comm:
ex = {
'zh_hk':[('Magpahinga muna tayo sa lilim.','多謝你幫忙。'),('Umupo ka sandali kung nahihilo ka.','我可唔可以問一個問題?'),('Paki-check po kung pumasok ang bayad.','我已經到咗。')],
'zh_tw':[('Magpahinga muna tayo sa lilim.','謝謝你的幫忙。'),('Umupo ka sandali kung nahihilo ka.','我可以問一個問題嗎?'),('Paki-check po kung pumasok ang bayad.','我已經到了。')],
}
else:
ex = {
'zh_hk':[('Sana maintindihan mo ako.','我希望你明白我。'),('Makikinig po ako nang may respeto.','我會帶住尊重去聽。'),('Tapat ang damdamin ko, all right.','我嘅感受已經好真誠。')],
'zh_tw':[('Sana maintindihan mo ako.','我希望你理解我。'),('Makikinig po ako nang may respeto.','我會帶著尊重傾聽。'),('Tapat ang damdamin ko, all right.','我的感受已經很真誠。')],
}
return ex[cfg_key]
def card_html(num, base, is_comm, cfg, lang_key):
l = cfg['labels']; meaning,nat,pol,friendly,playful,tone = base
g = ''.join(f"<li><strong>{html.escape(k)}:</strong> {html.escape(v)}</li>" for k,v in grammar_items(base, lang_key))
ex = ''.join(f"<li><strong>Tagalog:</strong> {html.escape(tl)}<br><strong>中文:</strong> {html.escape(cn)}</li>" for tl,cn in examples(is_comm, lang_key))
use = cfg['respect'] + ' ' + cfg['casual']
pron = 'Tagalog 母音可穩定朗讀:a=ah, e=eh, i=ee, o=oh, u=oo;po 輕聲,句尾保持清楚。'
if lang_key=='zh_hk': pron='Tagalog 母音可以穩定咁讀:a=ah, e=eh, i=ee, o=oh, u=oo;po 輕聲,句尾保持清楚。'
return f"""<article class='sentence-card' id='sentence-{num}'><h3>{cfg['sentence']} {num}</h3><dl class='template'>
<dt>{l[0]}:</dt><dd>{'活動現場練習句' if is_comm else '情感表達練習句'},用來比較自然、禮貌與 playful 語氣。</dd>
<dt>{l[1]}:</dt><dd>{html.escape(meaning)}</dd>
<dt>{l[2]}:</dt><dd lang='tl'>{html.escape(nat)}</dd>
<dt>{l[3]}:</dt><dd lang='tl'>{html.escape(pol)}</dd>
<dt>{l[4]}:</dt><dd>{html.escape(friendly)}</dd>
<dt>{l[5]}:</dt><dd>{html.escape(playful)}</dd>
<dt>{l[6]}:</dt><dd>{html.escape(tone)}</dd>
<dt>{l[7]}:</dt><dd>{html.escape(use)}</dd>
<dt>{l[8]}:</dt><dd><ul>{g}</ul></dd>
<dt>{l[9]}:</dt><dd>{'適合報到、工作坊、問路、感謝義工、等待協調與道別。' if is_comm else '適合學習柔和、尊重、有界線的情感表達。'}</dd>
<dt>{l[10]}:</dt><dd><ol>{ex}</ol></dd>
<dt>{l[11]}:</dt><dd>{html.escape(pron)}</dd>
<dt>{l[12]}:</dt><dd>{'先學自然 Tagalog,再學禮貌版本,最後才使用 有趣 Filipino-English。' if lang_key!='zh_hk' else '先學自然 Tagalog,再學禮貌版本,最後先用玩味 Filipino-English。'}</dd>
</dl></article>"""
created=[]
checks={}
for lang_key,cfg in lang_cfgs.items():
folder=Path(cfg['folder'])
if folder.exists(): shutil.rmtree(folder)
folder.mkdir()
(folder/'index.html').write_text(index_page(lang_key,cfg), encoding='utf-8')
for i,(slug,cat,trans) in enumerate(articles_base, start=1):
(folder/f'article-{i}-{slug}.html').write_text(article_page(i,slug,cat,trans,lang_key,cfg), encoding='utf-8')
zip_path=Path(cfg['zip'])
if zip_path.exists(): zip_path.unlink()
with zipfile.ZipFile(zip_path,'w',zipfile.ZIP_DEFLATED) as z:
for p in sorted(folder.glob('*.html'), key=lambda x:(x.name!='index.html', x.name)):
z.write(p, arcname=f"{folder.name}/{p.name}")
created.append(cfg['zip'])
html_files=list(folder.glob('*.html'))
checks[lang_key]={
'zip':cfg['zip'], 'zip_exists':zip_path.exists(), 'html_count':len(html_files),
'article_count':len(list(folder.glob('article-*.html'))),
'mobile_css_pages':sum('@media (max-width:860px)' in p.read_text(encoding='utf-8') for p in html_files),
'article_card_counts': {p.name:p.read_text(encoding='utf-8').count("class='sentence-card'") for p in folder.glob('article-*.html')},
'zip_size':zip_path.stat().st_size
}
print(json.dumps({'created':created,'checks':checks}, ensure_ascii=False, indent=2))
涵蓋內容:
- UI 在地化與內容在地化的差異
- 廣東話與台灣中文的語言設定
- zh-HK 與 zh-TW 的 HTML lang 處理
- 從既有 HTML 出發的替換對照表在地化流程
- Structured content generation using lang_cfgs, articles_base, comm_cards, and friendship_cards
- 在地化中文說明的同時,保留 Tagalog 作為學習目標
- 靜態 HTML 渲染、ZIP 封裝與驗證檢查
- 技術分享現場筆記與審閱檢查
現場筆記 1:UI 在地化不等於內容在地化
背景: 第一支腳本主要聚焦於 UI 替換:品牌文字、按鈕、分類、標籤、文章標題、摘要與共用教學文字。
目標: 翻譯可見介面,同時保留既有網站結構。
Prompt: 為既有 HTML 頁面建立語言字典與替換對照表。
結果: App 可以快速從既有 HTML 來源產生兩個在地化 ZIP 檔。
審閱檢查: 所有導覽標籤、文章標題、摘要與句子卡標籤是否都已在地化?
現場筆記 2:結構化產生更適合長期維護
背景: 替換對照表可行,但它依賴完全相同的來源字串。當來源 HTML 改變時,替換可能會無聲失敗。
目標: 將重要內容移入結構化多語資料。
Prompt: Build localized pages from lang_cfgs, articles_base, comm_cards, friendship_cards, helper functions, and templates.
結果: 因為在地化內容位於可預測的資料結構中,產生器變得更容易維護。
審閱檢查: 開發者是否能在不搜尋 HTML 的情況下更新單一文章標題?
現場筆記 3:保留學習目標
背景: 一般在地化流程可能會把所有東西都翻譯掉。但語言學習 App 不應這樣做。
目標: 保留 Tagalog 可見,同時翻譯周圍的中文說明。
Prompt: 在每張句子卡中保留 Natural Tagalog、Polite Tagalog、Friendly Filipino-English 與 Playful Filipino-English。
結果: 學習者仍可練習目標語言,同時用偏好的中文變體理解說明。
審閱檢查: 每張句子卡是否仍顯示 Tagalog 文字?
現場筆記 4:在地化語氣需要在地化聲音
背景: 廣東話與台灣中文不只是書寫風格不同,也可能在語氣、用字與節奏上不同。
目標: 讓每個在地化版本對目標讀者而言都自然。
Prompt: Use Cantonese-style wording for zh-HK and Taiwan Traditional Chinese for zh-TW.
結果: 三個網站會像各自獨立的在地化產品,而不是同一份通用中文轉換。
審閱檢查: Does the Cantonese version use Hong Kong-friendly wording such as 揀文章, 打開文章清單, and 用嚟練?
現場筆記 5:驗證是在地化的一部分
背景: 在地化建置可能因缺少檔案、缺少行動版 CSS、文章頁面不完整或語言標記錯誤而失敗。
目標: 將在地化視為建置產物,而不只是翻譯任務。
Prompt: 封裝每個在地化資料夾,並輸出 JSON 檢查結果。
結果: 開發者可以證明每個在地化版本都包含 25 個 HTML 檔案、24 個文章頁面、行動版 CSS,以及每篇文章的 40 張卡片。
審閱檢查: 建置輸出是否顯示所有三個語言版本都有預期數量?
技術分享角度
For a developer talk, the most important lesson is that localization can be engineered as a pipeline:
Language config
↓
文章中繼資料
↓
句子卡資料
↓
在地化標籤與例句
↓
靜態 HTML renderer
↓
每個語言的資料夾
↓
ZIP 封裝
↓
JSON 驗證檢查
這個流程容易說明、容易 Demo,也容易改造成其他教育型 App。
結語反思
這個在地化專案的核心能力不只是把英文翻譯成中文。真正的能力是設計一套可重複執行的在地化系統。專案將 UI 文字與學習內容分開、保留 Tagalog 作為目標語言、賦予每個中文變體自己的語氣、重複使用同一套響應式靜態版型、獨立封裝每個版本,並驗證輸出結果。這也是為什麼最終成果足以用於技術分享。
