Localization — Library สำหรับแอปพลิเคชันหลายภาษา

เลิกพัฒนาต่อแล้ว

นับตั้งแต่ Google เปิดตัว Android 13 ที่มาพร้อมกับ Per-app language preferences ที่รองรับบนแอนดรอยด์เวอร์ชันเก่าด้วย AndroidX จึงทำให้ไม่มีเหตุผลที่จะพัฒนา Library ตัวนี้อีกต่อไป

ดังนั้นขอแนะนำให้เปลี่ยนไปใช้ AndroidX จากทีม Google แทน โดยดูขั้นตอนการ Migrate ได้จาก Guide ตัวนี้

Localization/MIGRATE_TO_ANDROIDX.md at master · akexorcist/Localization
[Android] In-app language changing library. Contribute to akexorcist/Localization development by creating an account on GitHub.

การทำแอปพลิเคชันที่รองรับหลายภาษา ถือเป็นเรื่องธรรมดาที่จะต้องทำแอปพลิเคชันให้รองรับ ซึ่งมันก็ไม่ใช่เรื่องยากอะไร เพราะว่าแอนดรอยด์นั้นมี String Resource ให้ใช้เพื่อช่วยให้ชีวิตง่ายขึ้น แค่เตรียมข้อความสำหรับภาษาต่างๆไว้ แล้วระบบจะดึงมาแสดงตามภาษาของเครื่องเอง

แต่ปัญหาที่ยังเจอกันอยู่ก็คือ “การเปลี่ยนภาษาในระหว่างการใช้งานแอปพลิเคชัน” เพราะว่า String Resource นั้นถูกออกแบบมาโดยอ้างอิงกับภาษาของเครื่องเป็นหลัก แต่ถ้าต้องการให้แอปพลิเคชันสามารถเปลี่ยนภาษาระหว่างใช้งานอยู่ได้ ถือว่าเป็นอะไรที่ยุ่งยากไม่ใช่เล่น

แต่ว่าบทความนี้นี่แหละที่จะช่วยให้ชีวิตง่ายขึ้น หมดปัญหาเรื่องการเปลี่ยนภาษา เพราะเจ้าของบล็อกได้ทำ Library เพื่อแก้ปัญหาเรื่องนี้โดยมีชื่อว่า Localization

Localization เป็น Library ที่สร้างขึ้นมาเพื่อจัดการกับภาษาโดยที่นักพัฒนา “แทบจะไม่ต้องไปยุ่งอะไรเลย” เพราะว่าเบื้องหลังของ Library ตัวนี้เคลียร์ให้เรียบร้อยแล้ว~

คุณสมบัติของ Localization Activity

  • รองรับการเปลี่ยนภาษาระหว่างใช้งาน (On-time Changing)
  • กำหนดภาษาให้อัตโนมัติเมื่อ Activity เริ่มทำงาน
  • บันทึกลง Shared Preference ให้โดยอัตโนมัติ
  • ใช้งานง่ายมาก แทบจะไม่ต้องทำอะไร

วิธีการใช้งาน

สามารถดาวน์โหลดผ่าน Remote Dependencies ได้เลย โดยเพิ่ม Dependencies ลงไปดังนี้

implementation 'com.akexorcist:localizationactivity:1.2.2'

โปรเจคของผู้ที่หลงเข้ามาอ่านจะต้องสร้างคลาส Application ขึ้นมาเอง เพราะจะต้องใส่คำสั่งของคลาส LocalizationApplicationDelegate แบบนี้

import android.app.Application; 
import android.content.Context; 
import android.content.res.Configuration; 
import com.akexorcist.localizationactivity.core.LocalizationApplicationDelegate; 

public class CustomApplication extends Application { 
    LocalizationApplicationDelegate localizationDelegate = new LocalizationApplicationDelegate(this); 
    
    @Override
    protected void attachBaseContext(Context base) { 
        super.attachBaseContext(localizationDelegate.attachBaseContext(base)); 
    } 
    
    @Override 
    public void onConfigurationChanged(Configuration newConfig) { 
        super.onConfigurationChanged(newConfig); 
        localizationDelegate.onConfigurationChanged(this); } 
        
    @Override 
    public Context getApplicationContext() { 
        return localizationDelegate.getApplicationContext(super.getApplicationContext()); 
    } 
}

ส่วนวิธีการใช้งานนั้นลองดูตัวอย่างข้างล่างนี้ก่อนครับ

import android.os.Bundle;
import android.view.View;
import com.akexorcist.localizationactivity.ui.LocalizationActivity;

public class MainActivity extends LocalizationActivity implements View.OnClickListener {
    @Override
    public void onCreate(Bundle savedInstanceState) { 
         super.onCreate(savedInstanceState); 
         setContentView(R.layout.activity_simple); 
         findViewById(R.id.btn_th).setOnClickListener(this); 
         findViewById(R.id.btn_en).setOnClickListener(this); 
     } 
     
     @Override public void onClick(View v) { 
         int id = v.getId(); 
         if (id == R.id.btn_en) { 
             setLanguage("en"); 
         } else if (id == R.id.btn_th) { 
             setLanguage("th"); 
         } 
     }
 }

จากตัวอย่างข้างบนก็คือ Button สองตัวที่กดเพื่อเปลี่ยนภาษาไทยและอังกฤษแบบง่ายๆ โดยที่ Activity จะ Extend มาจาก LocalizationActivity อีกที

เท่านี้แหละครับ วิธีการใช้งาน Localization Activity

เห็นมั้ย ใช้โคตรง่าย!

นั่นล่ะครับ วิธีใช้งาน ง่ายมากจนเกือบจะไม่ต้องทำอะไรเลยใช่มั้ยล่ะ?

จากนั้นก็แค่สร้าง String Resource แยกเป็นสองภาษาระหว่างภาษาไทยกับอังกฤษซะ

เท่านี้ก็ได้แล้ว~ แอปพลิเคชันที่สามารถกดเปลี่ยนภาษาได้ในทันที โดยไม่ต้องเขียนอะไรเพิ่มให้วุ่นวาย

สืบทอดมาจาก AppCompatActivity

Library ตัวนี้สืบทอดมาจากคลาส AppCompatActivity เพราะงั้นพวกคำสั่งต่างๆใน Support v7 ก็เรียกใช้งานได้ปกติเลย

คำสั่งสำหรับ LocalizationActivity

คำสั่งใน LocalizationActivity จะมีน้อยมากครับ ทั้งนี้ก็เพราะว่าอยากจะให้มันเรียกใช้งานโดยไม่ต้องกำหนดหรือแก้ไขอะไรมากนัก ดังนั้น Method ที่ให้เรียกใช้งานก็จะมีแค่ 3 คำสั่ง

void setLanguage(String language) 
void setLanguage(String language, String country) 
String getLanguage() 
void setDefaultLanguage(String language) 
void setDefaultLanguage(String language, String country)

คำสั่ง setLanguage มีไว้กำหนดภาษาที่ต้องการจะเปลี่ยนนั่นเอง โดย String ก็คือภาษาที่ต้องการซึ่งจะถูกไปแปลงเป็นคลาส Locale เพื่อใช้กำหนดอีกทีหนึ่ง ดังนั้นตรงนี้ต้องกำหนดให้ถูกนะครับ ยกตัวอย่างเช่น

setLanguage("th") 
// Language : Thailand 

setLanguage("th", "TH") 
// Language : Thailand, Country : Thai 

setLanguage("en") 
// Language : English 

setLanguage("en", "GB") 
// Language : English, Country : Great Britain 

setLanguage("en", "US")
// Language : English, Country : United States

setLanguage(Locale.KOREA) 
// Language : Korean, Country : Korea 

setLanguage(Locale.KOREAN) 
// Language : Korean 

setLanguage(Locale.CANADA_FRENCH) 
// Language : French, Country : Canada

ดังนั้นตรงนี้ต้องกำหนดรูปแบบให้ถูกต้องด้วยนะครับ ส่วนคำสั่ง getLanguage ก็แค่ดึง String ว่าภาษาที่กำหนดเป็นภาษาอะไร

และมีคำสั่ง setDefaultLanguage เพื่อกำหนดภาษาเริ่มต้น โดยมีเงื่อนไขว่าใส่แค่ Activity ตัวแรกสุดที่ทำงานและใส่ใน onCreate ก่อนที่จะเรียกคำสั่ง super.onCreate

@Override
public void onCreate(Bundle savedInstanceState) { 
    setDefaultLanguage(Locale.JAPAN.toString()); 
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.activity_main); 
    /* ... */ 
}

และ LocalizationActivity มี Override Method อีก 2 ตัวคือ

void onBeforeLocaleChanged()
void onAfterLocaleChanged()

ทั้ง 2 ตัวนี้จะถูกเรียกเมื่อ Activity มีการเปลี่ยนภาษา เผื่อว่าผู้ที่หลงเข้ามาอ่านจะได้เช็คได้ และเอาไปใช้งานกับคำสั่งต่างๆที่ต้องการให้ทำงานเมื่อมีการเปลี่ยนภาษา

หลักการทำงานของ Localization Activity

เจ้า Library ตัวนี้จะใช้วิธีกำหนด Locale ของตัวแอปพลิเคชันแล้วทำการสร้าง Activity ขึ้นมาใหม่ เพื่อให้ภาษาที่แสดงอยู่นั้นเปลี่ยน ดังนั้นเมื่อไรที่เรียกใช้งานคำสั่ง setLanguage มันก็จะทำการกำหนด Locale แล้วเรียกคำสั่ง recreate เพื่อให้ Activity ปิดตัวลงแล้วเปิดขึ้นมาใหม่

รองรับแอนดรอยด์เวอร์ชันต่ำสุดที่ API 11

เนื่องมาจาก คำสั่ง recreate เป็นคำสั่งที่มาใน API 11 หรือ Honeycomb 3.0 นั่นแหละ จึงทำให้ Library ตัวนี้ไม่สามารถใช้กับเวอร์ชันที่ต่ำกว่านั้นได้

Lifecycle เมื่อมีการเปลี่ยนภาษา

เพื่อให้เข้าใจง่ายขึ้นว่า onBeforeLocaleChanged กับ onAfterLocaleChanged ทำงานเมื่อไร ลองดูภาพ Lifecycle ของ Localization Activity เมื่อเรียกใช้คำสั่ง setLanguage ดูครับ

จะเห็นว่ามันแค่เพิ่มเข้ามาแค่ตอนแรกและตอนสุดท้ายนั่นเอง เมื่อมีการเปลี่ยนภาษา onBeforeLocaleChanged จะทำงานก่อนที่ onPause และ onAfterLocaleChanged จะทำงานต่อจาก onResume เผื่อว่าผู้ที่หลงเข้ามาอ่านอยากจะให้ทำคำสั่งบางอย่างเมื่อเปลี่ยนภาษา

เปลี่ยนภาษาได้ทุกหน้าที่ใช้งาน ถึงแม้ว่าหน้านั้นจะเคยเปิดไว้แล้ว

คำสั่งเปลี่ยนภาษาทั่วไป มักจะมีปัญหากับ Activity ที่เคยเปิดทิ้งไว้ก่อนหน้า เช่น ผู้ใช้เปิด Activity ตัวแรกที่เป็นภาษาไทยไว้ แล้วไปเปิดหน้าถัดไปที่เป็นภาษาไทยอยู่ ซึ่งหน้าถัดไปนั้นดันสามารถเปลี่ยนภาษาได้

โดยปกติแล้วถ้าหน้าปลายทางมีการเปลี่ยนภาษา หน้าที่เปิดไว้ก่อนหน้านี้จะไม่เปลี่ยนภาษาตาม เพราะว่ามันซ้อนอยู่ใน Backstack

แต่สำหรับ Localization Activity ไม่มีปัญหาอะไร เพราะว่าเมื่อกดกลับมาเรื่อยๆ หน้าที่แสดง ณ ตอนนั้นก็จะเปลี่ยนภาษาให้ทันที

เพราะงั้น ขอแค่กำหนดให้ Activity ที่ใช้งาน Extend มาจาก Localization Activity ก็จะจัดการเรื่องการแสดงภาษาได้อย่างง่ายดาย

หน้าจอกระพริบสีดำเมื่อเปลี่ยนภาษา

เนื่องจากคำสั่ง recreate เป็นการปิด Activity ทิ้งแล้วสั่งให้ทำงานใหม่จึงเป็นเรื่องปกติที่จะเห็นว่าหน้าจอมันกระพริบตอนที่เปลี่ยนภาษา

ต้อง Save/Restore Instance ใน Activity ด้วย

เนื่องจาก Library ตัวนี้ใช้วิธี Recreate Activity ดังนั้นถ้ามีข้อมูลอยู่ใน Activity นั้นๆก็ควรทำการ Save/Restore ให้เรียบร้อยซะ เพื่อให้ข้อมูลยังสามารถแสดงได้ปกติเหมือนเดิม (ซึ่งเป็นเรื่องปกติที่ควรทำอยู่แล้ว เมื่อทำแอปพลิเคชันที่รองรับหน้าแนวนอนและแนวตั้ง)

ดังนั้นสิ่งที่ควรทำคือประกาศ onSaveInstance และ onRestoreInstance แล้วจัดการให้เรียบร้อยซะ

import android.os.Bundle; 
import android.view.View; 
import com.akexorcist.localizationactivity.ui.LocalizationActivity; 

public class MainActivity extends LocalizationActivity implements View.OnClickListener { 
    @Override public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
        // TODO Initial view and widget here 
        if (savedInstanceState == null) { 
            // TODO Activity first created 
        } else { 
            // TODO Activity recreated from screen orientation or change language 
        } 
    } 
    
    @Override 
    protected void onSaveInstanceState(Bundle outState) { 
        super.onSaveInstanceState(outState); 
        // TODO Save instance here 
    } 
    
    @Override 
    protected void onRestoreInstanceState(Bundle savedInstanceState) { 
        // TODO Restore instance here 
        super.onRestoreInstanceState(savedInstanceState); 
    } 
}

Fragment ก็เปลี่ยนภาษาตาม

ในกรณีที่เรียกใช้ LocalizationActivity แล้วใน Activity มีการเรียกใช้งาน Fragment เวลาที่ Activity เปลี่ยนภาษาก็จะทำสร้างสร้าง Activity ขึ้นมาใหม่อีกครั้ง รวมไปถึง Fragment ก็ถูกสร้างขึ้นใหม่ด้วยเช่นกันจึงทำให้ภาษาที่แสดงอยู่บน Fragment เปลี่ยนตามด้วยเช่นกัน

ดังนั้น Fragment ก็ควรจะต้อง Save/Restore Instance ให้เรียบร้อยด้วย ลองอ่านเรื่องนี้ได้ที่ Best Practices ของการ Save/Restore State ของ Activity และ Fragment

Best Practices ของการ Save/Restore State ของ Activity และ Fragment (StatedFragment deprecated แล้วจ้า)
รอบที่แล้วเรานำเสนอ วิธีการ Save/Restore Fragment State ด้วย StatedFragment ที่เราเขียนขึ้นมาไป ได้รับการตอบรับเยอะมาก ต้องขอขอบพระคุณทุกท่านครับอย่างไรก็ตาม StatedFragment เป็นการ Break P

ไม่อยากใช้ AppCompat v7? ใช้ Delegate แทนได้นะ

เพราะผู้ที่หลงเข้ามาอ่านบางคนไม่ต้องการใช้ AppCompatActivity ไม่ว่าจะสาเหตุอะไรก็ตาม ซึ่งไลบรารีตัวนี้ก็สามารถนำไปใช้งานกับ Activity แบบอื่นๆได้ตามต้องการ เพียงแค่ใช้ LocalizationDelegate แล้วประกาศคำสั่งต่างๆไว้ให้ครบตามที่กำหนดไว้ก็พอ

import android.app.Activity; 
import android.content.Context; 
import android.content.res.Resources; 
import android.os.Bundle; 
import com.akexorcist.localizationactivity.core.LocalizationActivityDelegate; 
import com.akexorcist.localizationactivity.core.OnLocaleChangedListener; 
import java.util.Locale; 

public abstract class CustomActivity extends Activity implements OnLocaleChangedListener { 
    private LocalizationActivityDelegate localizationDelegate = new LocalizationActivityDelegate(this); 
    
    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        localizationDelegate.addOnLocaleChangedListener(this); 
        localizationDelegate.onCreate(savedInstanceState); 
        super.onCreate(savedInstanceState); 
    } 
    
    @Override
    public void onResume() { 
        super.onResume(); 
        localizationDelegate.onResume(this); 
    } 
    
    @Override 
    protected void attachBaseContext(Context newBase) { 
        super.attachBaseContext(localizationDelegate.attachBaseContext(newBase)); 
    } 
    
    @Override 
    public Context getApplicationContext() { 
        return localizationDelegate.getApplicationContext(super.getApplicationContext()); 
    } 
    
    @Override public Resources getResources() { 
        return localizationDelegate.getResources(super.getResources()); 
    } 
        
    public final void setLanguage(String language) { 
        localizationDelegate.setLanguage(this, language); 
    } 
    
    public final void setLanguage(Locale locale) { 
        localizationDelegate.setLanguage(this, locale); 
    } 
    
    public final void setDefaultLanguage(String language) { 
        localizationDelegate.setDefaultLanguage(language); 
    } 
    
    public final void setDefaultLanguage(Locale locale) { 
        localizationDelegate.setDefaultLanguage(locale); 
    } 
    
    public final Locale getCurrentLanguage() { 
        return localizationDelegate.getLanguage(this); 
    } 
    
    // Just override method locale change event 
    @Override 
    public void onBeforeLocaleChanged() { } 
    
    @Override 
    public void onAfterLocaleChanged() { } 
}

เพียงเท่านี้ก็สามารถเอา Activity ตัวนี้ไปใช้งานได้เลย

และถ้าผู้ที่หลงเข้ามาอ่านไม่ได้ใช้ AppCompat v7 เลย ก็ให้กำหนดใน Gradle ด้วยว่าเอา AppCompat v7 ที่อยู่ในไลบรารีตัวนี้ออกด้วย เพื่อไม่ให้มี Method Count สิ้นเปลืองเกินจำเป็น

implementation ('com.akexorcist:localizationactivity:+') { 
    exclude module: 'appcompat-v7' 
}

ตัวอย่างการใช้งาน

เผื่อผู้ที่หลงเข้ามาอ่านนึกไม่ออกว่าเวลาใช้งานจะต้องเรียกใช้งานยังไงและจัดการกับเรื่อง Save/Restore Instance อย่างไร ซึ่งเจ้าของบล็อกก็มีตัวอย่างไว้ให้ดูเบื้องต้นแล้ว เข้าไปดูกันได้ที่ Android-LocalizationActivity [GitHub]

GitHub - akexorcist/Localization: [Android] In-app language changing library
[Android] In-app language changing library. Contribute to akexorcist/Localization development by creating an account on GitHub.

โดยโค้ดตัวอย่างจะแบ่งเป็น 3 แบบด้วยกันคือ

  • Activity ธรรมด๊าธรรมดา
  • Activity ที่ Custom Activity เอง ไม่ได้ใช้ Localization Activity โดยตรง
  • Activity ที่เปลี่ยนภาษาจากอีก Activity หนึ่ง
  • Activity ที่แปะ Fragment ไว้บนนั้น
  • Activity ที่มี Fragment ซ้อนอยู่ข้างใน Fragment อีกที
  • Activity ที่ข้างในมี View Pager

โดยทั้ง 3 ตัวอย่างนี้มีการ Save/Restore Instance ให้กับ Activity และ Fragment เรียบร้อยแล้ว ดังนั้นจึงรองรับทั้งการเปลี่ยนภาษาและการหมุนจอ โดยที่ยังทำงานได้ปกติ (แต่ Layout ไม่ได้จัดให้สวย เพราะงั้นอย่าซีเรียสกับหน้าตา)

จบจ้า