Localization — Library สำหรับแอปพลิเคชันหลายภาษา
เลิกพัฒนาต่อแล้ว
นับตั้งแต่ Google เปิดตัว Android 13 ที่มาพร้อมกับ Per-app language preferences ที่รองรับบนแอนดรอยด์เวอร์ชันเก่าด้วย AndroidX จึงทำให้ไม่มีเหตุผลที่จะพัฒนา Library ตัวนี้อีกต่อไป
ดังนั้นขอแนะนำให้เปลี่ยนไปใช้ AndroidX จากทีม Google แทน โดยดูขั้นตอนการ Migrate ได้จาก Guide ตัวนี้
การทำแอปพลิเคชันที่รองรับหลายภาษา ถือเป็นเรื่องธรรมดาที่จะต้องทำแอปพลิเคชันให้รองรับ ซึ่งมันก็ไม่ใช่เรื่องยากอะไร เพราะว่าแอนดรอยด์นั้นมี String Resource ให้ใช้เพื่อช่วยให้ชีวิตง่ายขึ้น แค่เตรียมข้อความสำหรับภาษาต่างๆไว้ แล้วระบบจะดึงมาแสดงตามภาษาของเครื่องเอง
แต่ปัญหาที่ยังเจอกันอยู่ก็คือ “การเปลี่ยนภาษาในระหว่างการใช้งานแอปพลิเคชัน” เพราะว่า String Resource นั้นถูกออกแบบมาโดยอ้างอิงกับภาษาของเครื่องเป็นหลัก แต่ถ้าต้องการให้แอปพลิเคชันสามารถเปลี่ยนภาษาระหว่างใช้งานอยู่ได้ ถือว่าเป็นอะไรที่ยุ่งยากไม่ใช่เล่น
แต่ว่าบทความนี้นี่แหละที่จะช่วยให้ชีวิตง่ายขึ้น หมดปัญหาเรื่องการเปลี่ยนภาษา เพราะเจ้าของบล็อกได้ทำ Library เพื่อแก้ปัญหาเรื่องนี้โดยมีชื่อว่า Localization
- บทความภาษาอังกฤษ
- บทความภาษาไทย [Now Reading]
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
ไม่อยากใช้ 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]
โดยโค้ดตัวอย่างจะแบ่งเป็น 3 แบบด้วยกันคือ
- Activity ธรรมด๊าธรรมดา
- Activity ที่ Custom Activity เอง ไม่ได้ใช้ Localization Activity โดยตรง
- Activity ที่เปลี่ยนภาษาจากอีก Activity หนึ่ง
- Activity ที่แปะ Fragment ไว้บนนั้น
- Activity ที่มี Fragment ซ้อนอยู่ข้างใน Fragment อีกที
- Activity ที่ข้างในมี View Pager
โดยทั้ง 3 ตัวอย่างนี้มีการ Save/Restore Instance ให้กับ Activity และ Fragment เรียบร้อยแล้ว ดังนั้นจึงรองรับทั้งการเปลี่ยนภาษาและการหมุนจอ โดยที่ยังทำงานได้ปกติ (แต่ Layout ไม่ได้จัดให้สวย เพราะงั้นอย่าซีเรียสกับหน้าตา)
จบจ้า