บทที่ 16: Multi-language (i18n) Integration
สารบัญเนื้อหา
บทที่ 16: Multi-language (i18n) Integration
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- สร้างระบบเปลี่ยนภาษา (ไทย/อังกฤษ) ภายในแอปแบบ Real-time
- จัดการไฟล์ข้อความ (Strings) แยกตามภาษาอย่างเป็นระเบียบ
- ใช้
LanguageControllerเพื่อจำค่าภาษาที่ผู้ใช้เลือกไว้ - เปลี่ยนภาษาของแอปได้โดยไม่ต้อง Restart แอปใหม่
📚 ทฤษฎีและแนวคิด
1. i18n คืออะไร?
i18n ย่อมาจาก Internationalization (ระหว่างตัว i และ n มี 18 ตัวอักษร) คือกระบวนการเตรียมแอปให้รองรับหลายภาษา
2. แนวทางของ QUOM
ปกติ Flutter มี package มาตรฐานชื่อ flutter_localizations แต่ setup ค่อนข้างยุ่งยากสำหรับมือใหม่ (ต้องทำ arb file, command line generator)
ใน QUOM เราจะใช้แนวทาง "Simple Custom Localization":
- สร้าง Class ธรรมดาที่มีตัวแปร Static
- ใช้
ValueNotifierเพื่อบอก UI ให้รีเฟรชเมื่อภาษาเปลี่ยน - วิธีนี้ง่าย ควบคุมได้ 100% และไม่ต้องใช้ Code Generator
💻 การเขียนโค้ด (Implementation)
1. สร้างคลังคำศัพท์ (AppStrings)
ไฟล์: mobile/lib/services/app_strings.dart
เราจะเก็บคำแปลทั้งหมดไว้ที่นี่ โดยใช้ "Key" เป็นตัวกลาง
import 'language_controller.dart'; // เดี๋ยวเราจะสร้างไฟล์นี้
class AppStrings {
// ฟังก์ชันช่วยเช็คว่าเป็นภาษาไทยไหม
static bool get isThai => LanguageController.instance.locale.value.languageCode == 'th';
// --- 1. นิยาม Dictionary ---
static const Map<String, Map<String, String>> _localizedValues = {
'en': {
'app_name': 'QUOM',
'home_title': 'My Notes',
'settings_title': 'Settings',
'language': 'Language',
'dark_mode': 'Dark Mode',
'login': 'Login',
'logout': 'Logout',
'confirm_logout': 'Are you sure you want to logout?',
},
'th': {
'app_name': 'QUOM',
'home_title': 'บันทึกของฉัน',
'settings_title': 'การตั้งค่า',
'language': 'ภาษา',
'dark_mode': 'โหมดมืด',
'login': 'เข้าสู่ระบบ',
'logout': 'ออกจากระบบ',
'confirm_logout': 'คุณแน่ใจหรือไม่ที่จะออกจากระบบ?',
},
};
// --- 2. ฟังก์ชันแปลภาษา (Magic Function) ---
static String tr(String key) {
// ดึงรหัสภาษาปัจจุบัน (เช่น 'th' หรือ 'en')
final lang = LanguageController.instance.locale.value.languageCode;
// ค้นหาคำแปล (ถ้าไม่มีในภาษานั้น ให้ใช้ภาษาอังกฤษเป็น Default)
return _localizedValues[lang]?[key] ?? _localizedValues['en']?[key] ?? key;
}
// --- 3. Getters เพื่อความสะดวกในการเรียกใช้ (Optional) ---
// จะได้พิมพ์ AppStrings.homeTitle แทน AppStrings.tr('home_title')
static String get homeTitle => tr('home_title');
static String get settingsTitle => tr('settings_title');
static String get language => tr('language');
static String get login => tr('login');
static String get logout => tr('logout');
}
2. สร้าง Language Controller
ไฟล์: mobile/lib/services/language_controller.dart
ทำหน้าที่จัดการ State ว่าตอนนี้ใช้ภาษาอะไรอยู่ และบันทึกลงเครื่อง
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LanguageController {
อธิบายทีละบรรทัด (Imports):
-
import 'package:flutter/material.dart'- ใช้สำหรับ Class
LocaleและValueNotifier
- ใช้สำหรับ Class
-
import 'package:shared_preferences/shared_preferences.dart'- ใช้สำหรับจำค่าภาษาที่เลือกไว้ (ไม่ให้เปลี่ยนกลับเป็นไทยทุกครั้งที่เปิดแอป)
อธิบาย: // Singleton static final LanguageController instance = LanguageController.(); LanguageController.();
// ตัวแปร State (เริ่มที่ภาษาไทย) final ValueNotifier<Locale> locale = ValueNotifier(const Locale('th'));
// โหลดค่าล่าสุดตอนเปิดแอป Future<void> init() async { final prefs = await SharedPreferences.getInstance(); final savedCode = prefs.getString('language_code');
if (savedCode != null) {
locale.value = Locale(savedCode);
}
}
// เปลี่ยนภาษา Future<void> changeLanguage(String code) async { // code = 'th' หรือ 'en' locale.value = Locale(code);
// บันทึกลงเครื่อง
final prefs = await SharedPreferences.getInstance();
await prefs.setString('language_code', code);
} }
### 3. การใช้งานในหน้า Settings UI
ไฟล์: `mobile/lib/screens/settings_screen.dart`
จุดสำคัญคือการหุ้ม Widget ด้วย `ValueListenableBuilder` เพื่อให้มัน Rebuild เมื่อภาษาเปลี่ยน
```dart
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
// --- ฟังการเปลี่ยนแปลงของภาษา ---
return ValueListenableBuilder<Locale>(
valueListenable: LanguageController.instance.locale,
builder: (context, locale, child) {
return Scaffold(
appBar: AppBar(
// เรียกใช้คำแปลผ่าน AppStrings
title: Text(AppStrings.settingsTitle),
),
body: ListView(
children: [
// ส่วนเลือกภาษา
ListTile(
leading: Icon(Icons.language),
title: Text(AppStrings.language),
trailing: DropdownButton<String>(
value: locale.languageCode,
items: const [
DropdownMenuItem(value: 'en', child: Text('English')),
DropdownMenuItem(value: 'th', child: Text('ไทย')),
],
onChanged: (value) {
if (value != null) {
LanguageController.instance.changeLanguage(value);
}
},
),
),
Divider(),
// ปุ่ม Logout
ListTile(
leading: Icon(Icons.exit_to_app, color: Colors.red),
title: Text(AppStrings.logout, style: TextStyle(color: Colors.red)),
onTap: () {
// Show Dialog...
},
),
],
),
);
},
);
}
}
4. การจัดการ Font (สำคัญ!)
ภาษาไทยต้องการฟอนต์เฉพาะ (เช่น Sarabun, Prompt, Kanit) เพื่อความสวยงาม ถ้าใช้ฟอนต์ Default ของ Android/iOS สระอาจลอยหรือดูไม่สวย
ใน pubspec.yaml:
fonts:
- family: Sarabun
fonts:
- asset: assets/fonts/Sarabun-Regular.ttf
- asset: assets/fonts/Sarabun-Bold.ttf
weight: 700
ใน AppTheme:
ThemeData(
fontFamily: 'Sarabun', // ใช้ทั้งแอป
)
เทคนิคขั้นสูง (แยกฟอนต์ตามภาษา):
TextStyle get dynamicFontStyle {
if (AppStrings.isThai) {
return TextStyle(fontFamily: 'Sarabun'); // ไทยใช้สารบรรณ
} else {
return TextStyle(fontFamily: 'Roboto'); // Eng ใช้ Roboto
}
}
📝 แบบฝึกหัด
โจทย์ที่ 1: Adding a New Language
สิ่งที่ต้องทำ:
- เพิ่ม key
'cn'(จีน) หรือ'jp'(ญี่ปุ่น) ใน_localizedValuesในAppStrings - เพิ่มตัวเลือกใน Dropdown หน้า Settings
- ทดสอบเปลี่ยนภาษาที่ 3
โจทย์ที่ 2: Dynamic Date Format
สิ่งที่ต้องทำ:
- ใช้วันที่ปัจจุบัน
DateTime.now() - ถ้าเป็น
'th': แสดง "20 มกราคม 2568" (ต้องบวกปี 543) - ถ้าเป็น
'en': แสดง "January 20, 2025" - ทิป: ใช้ package
intlช่วย format
โจทย์ที่ 3: Arguments in Strings
สิ่งที่ต้องทำ:
- บางประโยคมีตัวแปร เช่น "สวัสดีคุณ [ชื่อ]"
- สร้างฟังก์ชัน
trWithArgs(String key, List<String> args) - แทนที่
{0},{1}ใน string ด้วยค่าจริง
📌 สรุป
ระบบ i18n ที่เราสร้างขึ้นเองนี้มีความยืดหยุ่นสูง:
- Centralized: ทุกคำรวมอยู่ที่
AppStrings - Reactive: เปลี่ยนปุ๊บ หน้าจอเปลี่ยนปั๊บด้วย
ValueNotifier - Persistent: จำค่าภาษาไว้ให้ด้วย
SharedPreferences
ไฟล์ที่เกี่ยวข้อง
mobile/lib/services/app_strings.dartmobile/lib/services/language_controller.dartmobile/lib/screens/settings_screen.dart
🔜 บทถัดไป
บทที่ 17: Complete Authentication Flow (บทนี้ได้รับการปรับปรุงเรียบร้อยแล้วในก่อนหน้านี้)
บทที่ 18: Real-time Features (Chat) เราจะข้ามไปดูการทำระบบแชทโต้ตอบกับ AI กันต่อเลย!