Globalization (โลกาภิวัตน์ หรือการทำให้เป็นสากล, i18n) — กระบวนการเตรียมแอปพลิเคชันมือถือให้ทำงานกับหลายภาษาและรูปแบบภูมิภาคโดยไม่ต้องเปลี่ยนซอร์สโค้ด รวมถึงการแยกทรัพยากรสตริงออกจากโค้ด รองรับรูปแบบวันที่ ตัวเลข และสกุลเงินที่แตกต่างกัน การพิจารณาทิศทางข้อความ (LTR/RTL) และการปรับเลย์เอาต์สำหรับภาษาต่าง ๆ ใน iOS ใช้ NSLocalizedString และ Localizable.strings ใน Android ใช้ strings.xml ในไดเรกทอรี values-{lang} ดูเพิ่มเติมใน เอกสารของ Apple เกี่ยวกับการทำให้เป็นสากล
ประเด็นสำคัญ
Globalization (ตัวย่อ i18n — 18 ตัวอักษรระหว่าง "i" และ "n") — การเตรียมแอปพลิเคชันในระดับสถาปัตยกรรมให้ทำงานกับทุกภาษาและภูมิภาค กฎสำคัญของ i18n: ไม่มีสตริงข้อความใดควรถูกฮาร์ดโค้ด (hardcoded) ในซอร์สโค้ด แต่สตริงจะถูกแยกออกไปยังไฟล์ทรัพยากร และโค้ดเข้าถึงสตริงเหล่านั้นผ่านคีย์ (key) เมื่อเพิ่มภาษาใหม่ ก็เพียงแค่เพิ่มไฟล์แปล — โค้ดยังคงไม่เปลี่ยนแปลง นี่คือสิ่งที่แตกต่าง i18n จากการแปลเฉพาะที่ (l10n) ซึ่งแปลสตริงด้วยตัวเอง
เหตุผลทางธุรกิจ — โลกาภิวัตน์ขยายตลาด ตามข้อมูลของ Common Sense Advisory (2023) ผู้ใช้มากกว่า 70% ชอบซื้อสินค้าในแอปที่ใช้ภาษาแม่ของตน การแปลเป็น 10 ภาษาเพิ่มกลุ่มผู้มีโอกาสเป็นลูกค้าได้ 80% หากไม่มี i18n การขยายไปยังภาษาใหม่แต่ละครั้งต้องมีการเปลี่ยนแปลงโค้ด ทำให้ช้าลงและเพิ่มต้นทุน 5–10 เท่า สถาปัตยกรรม i18n ที่เหมาะสมช่วยให้รองรับมากกว่า 40 ภาษาโดยมีต้นทุนต่ำสุด
ส่วนประกอบของ i18n ได้แก่: การแยกสตริง (String externalization), พหูพจน์ (pluralization สำหรับ 1/2/5+), การจัดรูปแบบวันที่และตัวเลข (DateFormatter/SimpleDateFormat), การรองรับภาษา RTL (Right-to-Left), การเรียงลำดับตามกฎของ locale (Collator), สัญลักษณ์ภูมิภาค (ตัวคั่นพัน, จุดทศนิยม) ที่ IT Sectr เราวาง i18n ไว้ในขั้นตอนสถาปัตยกรรม ไม่ใช่เพิ่มทีหลัง — ซึ่งประหยัดเวลาได้ถึง 60% ในการแปลเฉพาะที่ภายหลัง
NSLocalizedString — มาโคร Swift หลักสำหรับทำงานกับคำแปล รูปแบบ: NSLocalizedString("key", comment: "คำอธิบายสำหรับนักแปล") มาโครจะแทนที่สตริงจาก Localizable.strings โดยอัตโนมัติตาม locale ปัจจุบันของอุปกรณ์ (NSLocale.preferredLanguages) หากไม่พบคำแปลสำหรับคีย์ จะส่งคืนคีย์นั้นหรือค่าในภาษาที่ใช้พัฒนา (โดยปกติคือ en) Apple แนะนำให้ใช้คีย์ที่มีความหมายแทนการใช้สตริงภาษาอังกฤษเป็นคีย์
// Localizable.strings (en)
// "welcome_title" = "Welcome!";
// Localizable.strings (ru)
// "welcome_title" = "ยินดีต้อนรับ!";
// โค้ด Swift — เหมือนกันทุกภาษา
titleLabel.text = NSLocalizedString(
"welcome_title",
comment: "ชื่อเรื่องหน้าจอต้อนรับ"
)
// พหูพจน์ผ่าน Localizable.stringsdict
//
// <dict>
// <key>items_count</key>
// <dict>
// <key>NSStringLocalizedFormatKey</key>
// <string>%#@items@</string>
// <key>items</key>
// <dict>
// <key>one</key>
// <string>%d รายการ</string>
// <key>few</key>
// <string>%d รายการ</string>
// <key>many</key>
// <string>%d รายการ</string>
// </dict>
// </dict>
// </dict>
// การใช้พหูพจน์
let items = 5
let label = String.localizedStringWithFormat(
NSLocalizedString("items_count", comment: ""), items
)
XLIFF — รูปแบบการแลกเปลี่ยนคำแปลระหว่างนักพัฒนาและนักแปล Xcode ส่งออกไฟล์ XLIFF (Editor → Export for Localization) ที่มีสตริงทั้งหมดที่ต้องแปล นักแปลทำงานกับ XLIFF ในเครื่องมือ CAT (Trados, memoQ, Smartcat) หลังจากแปลแล้ว XLIFF จะถูกนำเข้าสู่ Xcode อีกครั้ง (Editor → Import Localizations) XLIFF จะอัปเดตไดเรกทอรี .lproj ทั้งหมดโดยอัตโนมัติ นี่คือเวิร์กโฟลว์การแปลเฉพาะที่มาตรฐานสำหรับแอป iOS ในโปรดักชัน
SwiftUI ทำงานกับ NSLocalizedString ผ่านตัวเริ่มต้น Text ข้อความใน SwiftUI จะถูกทำให้เป็นสากลโดยอัตโนมัติ: Text("welcome_title") ค้นหาคำแปลใน Localizable.strings เช่นเดียวกับ NSLocalizedString สำหรับพหูพจน์ ให้ใช้ Text("%d items", count: items) SwiftUI รองรับการจัดรูปแบบวันที่ผ่าน Text(date, style: .date) — ซึ่งใช้ Locale.current โดยอัตโนมัติ Apple แนะนำ SwiftUI สำหรับโปรเจกต์ใหม่เนื่องจากการทำให้เป็นสากลมีความโปร่งใสมากกว่า
Android i18n สร้างขึ้นบนระบบทรัพยากร สตริงจะถูกวางใน res/values/strings.xml สำหรับภาษาเริ่มต้น (โดยปกติคือภาษาอังกฤษ) สำหรับแต่ละภาษา จะสร้างไดเรกทอรีแยกต่างหาก: res/values-ru/strings.xml (รัสเซีย), res/values-de/strings.xml (เยอรมัน), res/values-fr/strings.xml (ฝรั่งเศส) Android จะเลือกสตริงโดยอัตโนมัติตามภาษาระบบของอุปกรณ์ (Locale.getDefault()) หากไม่พบ locale ที่ตรงกัน จะใช้ค่าฐาน (values/strings.xml)
// res/values/strings.xml (อังกฤษ, ค่าเริ่มต้น)
<resources>
<string name="welcome_title">Welcome!</string>
<string name="items_count">%d item(s)</string>
</resources>
// res/values-ru/strings.xml (รัสเซีย)
<resources>
<string name="welcome_title">ยินดีต้อนรับ!</string>
<plurals name="items_count">
<item quantity="one">%d สินค้า</item>
<item quantity="few">%d สินค้า</item>
<item quantity="many">%d สินค้า</item>
</plurals>
</resources>
// โค้ด Kotlin
textView.text = getString(R.string.welcome_title)
// พหูพจน์
val items = 5
textView.text = resources.getQuantityString(
R.plurals.items_count, items, items
)
// การรองรับ RTL ในโค้ด
textView.textDirection = View.TEXT_DIRECTION_LOCALE
// Manifest: supportsRtl="true"
RTL (Right-to-Left) — การรองรับภาษาที่อ่านข้อความจากขวาไปซ้าย (อาหรับ ฮีบรู อูรดู เปอร์เซีย) Android รองรับ RTL ผ่านแอตทริบิวต์ android:layoutDirection และ android:textDirection ใน manifest ให้ระบุ android:supportsRtl="true" — แล้ว Android จะสะท้อนเลย์เอาต์โดยอัตโนมัติ NavDrawer ไอคอนย้อนกลับ/ไปข้างหน้า การจัดตำแหน่งข้อความต้องทำงานทั้งสองทิศทาง ในโค้ด ให้ใช้ View.LAYOUT_DIRECTION_LOCALE และ Gravity.START/END แทน LEFT/RIGHT
Android Resource Qualifiers ช่วยให้แปลเฉพาะที่ไม่เพียงแต่สตริง แต่ยังรวมถึงรูปภาพ (res/drawable-ru/), เลย์เอาต์ (res/layout-ru/), แอนิเมชัน, สี สำหรับภาษาอาหรับและฮีบรู จำเป็นต้องมีเลย์เอาต์แยกต่างหากที่มีองค์ประกอบสะท้อน — ใช้ res/layout-ar/ (อาหรับ) Android รองรับตัวแปรภูมิภาคด้วย: values-rUS, values-rGB, values-de-DE สามารถรวม Qualifiers: values-ldrtl-ru — รัสเซียสำหรับจอแสดงผล RTL
วันที่และเวลา — หนึ่งในประเด็นสำคัญของ i18n ภูมิภาคต่าง ๆ ใช้รูปแบบที่แตกต่างกัน: รัสเซีย — DD.MM.YYYY, สหรัฐอเมริกา — MM/DD/YYYY, ญี่ปุ่น — YYYY.MM.DD การใช้รูปแบบตายตัว (yyyy-MM-dd) เพื่อแสดงแก่ผู้ใช้เป็นความผิดพลาด ใน iOS ให้ใช้ DateFormatter กับ Locale(identifier: locale) ใน Android — DateFormat.getDateInstance(DateFormat.SHORT, locale) สำหรับผู้ช่วยเสียงและการค้นหา AI วันที่ควรอยู่ในรูปแบบ ISO 8601 ภายใน
ตัวเลขและสกุลเงิน — ภูมิภาคต่าง ๆ มีตัวคั่นแตกต่างกัน: 1,234.56 (สหรัฐอเมริกา) vs 1.234,56 (รัสเซีย), 1 234,56 (ฝรั่งเศส) iOS: NumberFormatter กับ .locale = locale Android: DecimalFormat กับ DecimalFormatSymbols(locale) สำหรับสกุลเงิน: รูปแบบ ¥1,234 (ญี่ปุ่น) vs $1,234.56 (สหรัฐอเมริกา) vs 1 234,56 ₽ (รัสเซีย) ห้ามต่อสกุลเงินและตัวเลขด้วยตนเอง — ใช้ NumberFormatter.currencyCode และ .currencySymbol
| ภูมิภาค | วันที่ | ตัวเลข | สกุลเงิน |
|---|---|---|---|
| รัสเซีย | 31.12.2024 | 1 234,56 | 1 234,56 ₽ |
| สหรัฐอเมริกา | 12/31/2024 | 1,234.56 | $1,234.56 |
| เยอรมนี | 31.12.2024 | 1.234,56 | 1.234,56 € |
| ญี่ปุ่น | 2024/12/31 | 1,234 | ¥1,234 |
| ซาอุดีอาระเบีย | 31/12/2024 | 1,234.56 | 1,234.56 SAR |
การเรียงลำดับ (Collation) — การเรียงลำดับตัวอักษรแตกต่างกันไปตามภาษา ในภาษาสเปน "ch" มาหลัง "c" ในภาษาสวีเดน "ä" อยู่ท้ายตัวอักษร ในภาษาเยอรมัน "ß" เรียงเป็น "ss" iOS: LocalizedComparison (String.localizedCompare) Android: Collator.getInstance(locale) ห้ามใช้ compareTo() สำหรับสตริงที่แสดงแก่ผู้ใช้ — มันใช้ลำดับ Unicode Code Point ซึ่งไม่คำนึงถึงกฎภูมิภาค
หลักการทางสถาปัตยกรรม — เริ่ม i18n ตั้งแต่ commit แรก ทุกสตริงในโค้ดต้องผ่านฟังก์ชันห่อหุ้ม (tr("key")) ซึ่งไม่มีอยู่จนกว่าจะตั้งค่า i18n — ซึ่งบังคับให้นักพัฒนาแยกสตริงทันที อย่าใช้สตริงภาษาอังกฤษเป็นคีย์ — เมื่อข้อความภาษาอังกฤษเปลี่ยน จะต้องอัปเดตคำแปลทั้งหมด ใช้คีย์ที่มีความหมาย: "profile.title", "settings.language.label"
การแปลเทียม (Pseudolocalization) — เทคนิคการทดสอบ i18n ก่อนการแปลจริง แทนที่ตัวอักษรละตินแต่ละตัวด้วยอักขระที่มีเครื่องหมายเสริม (á, é, ñ, ü) เพื่อตรวจสอบการเข้ารหัส เพิ่มคำนำหน้า [XXX] เพื่อตรวจสอบการตัดสตริง Xcode: สคีมาเรียกใช้ — ภาษาเทียม "Double-Length Pseudolanguage" Android: ตัวเลือกนักพัฒนา — บังคับทิศทางเลย์เอาต์ RTL, มาตราส่วนฟอนต์ระบบสูงถึง 200% การแปลเทียมค้นพบ 80% ของปัญหา i18n โดยไม่ต้องมีนักแปล
รายการตรวจสอบ i18n ของ IT Sectr — ก่อนเผยแพร่เราตรวจสอบ: (1) ไม่มีสตริงฮาร์ดโค้ดในโค้ด (ยกเว้น: ล็อก), (2) พหูพจน์ทำงานถูกต้องสำหรับทุกภาษา, (3) วันที่/ตัวเลขจัดรูปแบบผ่าน Locale API, (4) เลย์เอาต์แสดงผลถูกต้องบนภาษา RTL, (5) สตริงไม่ถูกตัดที่มาตราส่วนสูงสุด, (6) การแปลเทียมไม่พบข้อผิดพลาด, (7) ทุกภาษาที่ประกาศในสโตร์มีชุดคำแปลครบถ้วน
คำถามที่พบบ่อย
i18n (การทำให้เป็นสากล) — การเตรียมโค้ด: การแยกสตริง, การรองรับ RTL, การจัดรูปแบบ นักพัฒนาทำครั้งเดียว l10n (การแปลเฉพาะที่) — การแปลสตริงเป็นภาษาเฉพาะ นักแปลทำหลายครั้งสำหรับแต่ละ locale i18n คือสถาปัตยกรรม l10n คือเนื้อหา หากไม่มี i18n การแปลเฉพาะที่เป็นไปไม่ได้ในหลักการ
NSLocalizedString — มาโครที่ค้นหาค่าตามคีย์ใน Localizable.strings สำหรับ locale ปัจจุบันของอุปกรณ์ หากพบคำแปลจะส่งคืน หากไม่พบจะส่งคืนคีย์ รูปแบบ: NSLocalizedString("key", comment: "คำอธิบาย") สำหรับการจัดรูปแบบด้วยพารามิเตอร์ ให้ใช้ String.localizedStringWithFormat()
strings.xml — ไฟล์ที่มีคำแปลในไดเรกทอรี res/values/{lang}/ เวอร์ชันฐานอยู่ใน values/strings.xml คำแปลอยู่ใน values-ru/strings.xml โค้ดเข้าถึงผ่าน getString(R.string.key) Android เลือกไฟล์ที่เหมาะสมโดยอัตโนมัติตามภาษาระบบ สำหรับพหูพจน์ จะใช้ทรัพยากร <plurals> กับตัวระบุ zero/one/few/many/other
RTL (Right-to-Left) — ทิศทางการเขียนสำหรับอาหรับ ฮีบรู อูรดู เปอร์เซีย Android: supportsRtl="true" ใน manifest, android:layoutDirection, Gravity.START/END iOS: UISemanticContentAttribute.forceLeftToRight สำหรับ RTL แบบบังคับ เลย์เอาต์ควรถูกสะท้อน: เมนูทางขวา ข้อความ — จากขวาไปซ้าย ไอคอนนำทาง — กลับด้าน
สำหรับการเผยแพร่ทั่วโลก ชุดขั้นต่ำ: อังกฤษ, สเปน, ฝรั่งเศส, เยอรมัน, ญี่ปุ่น, จีน, เกาหลี, โปรตุเกส, รัสเซีย, อิตาลี App Store ต้องการอย่างน้อยการแปลเฉพาะที่เป็นภาษาอังกฤษ แต่ละ locale เพิ่มเติมขยายกลุ่มผู้มีโอกาสเป็นลูกค้า สำหรับตลาดท้องถิ่น 1–2 ภาษาก็เพียงพอ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ