บทที่ 16mobile|12 KB

บทที่ 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
  • 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

สิ่งที่ต้องทำ:

  1. เพิ่ม key 'cn' (จีน) หรือ 'jp' (ญี่ปุ่น) ใน _localizedValues ใน AppStrings
  2. เพิ่มตัวเลือกใน Dropdown หน้า Settings
  3. ทดสอบเปลี่ยนภาษาที่ 3

โจทย์ที่ 2: Dynamic Date Format

สิ่งที่ต้องทำ:

  1. ใช้วันที่ปัจจุบัน DateTime.now()
  2. ถ้าเป็น 'th': แสดง "20 มกราคม 2568" (ต้องบวกปี 543)
  3. ถ้าเป็น 'en': แสดง "January 20, 2025"
  4. ทิป: ใช้ package intl ช่วย format

โจทย์ที่ 3: Arguments in Strings

สิ่งที่ต้องทำ:

  1. บางประโยคมีตัวแปร เช่น "สวัสดีคุณ [ชื่อ]"
  2. สร้างฟังก์ชัน trWithArgs(String key, List<String> args)
  3. แทนที่ {0}, {1} ใน string ด้วยค่าจริง

📌 สรุป

ระบบ i18n ที่เราสร้างขึ้นเองนี้มีความยืดหยุ่นสูง:

  • Centralized: ทุกคำรวมอยู่ที่ AppStrings
  • Reactive: เปลี่ยนปุ๊บ หน้าจอเปลี่ยนปั๊บด้วย ValueNotifier
  • Persistent: จำค่าภาษาไว้ให้ด้วย SharedPreferences

ไฟล์ที่เกี่ยวข้อง


🔜 บทถัดไป

บทที่ 17: Complete Authentication Flow (บทนี้ได้รับการปรับปรุงเรียบร้อยแล้วในก่อนหน้านี้)

บทที่ 18: Real-time Features (Chat) เราจะข้ามไปดูการทำระบบแชทโต้ตอบกับ AI กันต่อเลย!