Internationalization #
Dalam dunia pengembangan aplikasi seluler berskala global, internasionalisasi (internationalization, disingkat i18n) dan lokalisasi (localization, disingkat l10n) bukan sekadar fitur pemanis aplikasi, melainkan suatu kebutuhan dasar. Internasionalisasi adalah proses merancang dan mempersiapkan aplikasi kita agar dapat beradaptasi dengan berbagai bahasa dan konvensi regional tanpa perlu menulis ulang kode sumber. Lokalisasi adalah proses aktual menerjemahkan teks, menyesuaikan format angka, mata uang, dan tanggal, serta mengadaptasi elemen tata letak sesuai dengan wilayah (locale) tertentu.
Menyiapkan sistem i18n sejak hari pertama pengembangan aplikasi jauh lebih mudah dan efisien dibandingkan kita harus melakukan refaktorisasi massal pada ratusan string statis yang terlanjur tertulis keras (hardcoded) di dalam widget tree saat aplikasi sudah berukuran besar.
Pendahuluan: Mengapa i18n & l10n itu Penting #
Flutter dirancang sejak awal dengan dukungan lokalisasi kelas satu. Menggunakan library resmi dari Flutter memiliki beberapa keunggulan utama dibandingkan membuat sistem lokalisasi kustom sendiri:
- Kepatuhan Standar Industri: Flutter mengadopsi format ARB (Application Resource Bundle) yang berbasis JSON dan standar ICU (International Components for Unicode) untuk pluralisasi dan lokalisasi bersyarat.
- Type Safety: Generator kode Flutter (
gen-l10n) mengubah berkas ARB menjadi kelas Dart bertipe aman (strongly-typed). Kita tidak perlu memanggil string menggunakan key string (seperti'welcome_message'), melainkan langsung memanggil metode Dart (sepertil10n.welcomeMessage). Jika terjadi kesalahan ketik, kompilator Dart akan langsung mendeteksinya saat build time. - Dukungan Widget Bawaan: Library lokalisasi otomatis menyesuaikan teks internal pada komponen bawaan Flutter, seperti DatePicker, Calendar, TimePicker, dan dialog sistem.
Berikut adalah diagram alur proses translasi, kompilasi, dan visualisasi konsumsi berkas lokalisasi dalam siklus hidup aplikasi Flutter kita:
flowchart TD
subgraph InputFiles["1. Berkas Definisi (Input)"]
ARB_ID["app_id.arb (Template)"]
ARB_EN["app_en.arb"]
ARB_AR["app_ar.arb"]
end
subgraph CodeGen["2. Generator Kode (Compile-time)"]
L10N_Yaml["l10n.yaml Configuration"] --> GenTool["flutter gen-l10n Tool"]
ARB_ID --> GenTool
ARB_EN --> GenTool
ARB_AR --> GenTool
GenTool --> GenClass["AppLocalizations (Dart generated classes)"]
end
subgraph Runtime["3. Eksekusi Runtime (Widget Tree)"]
GenClass -->|"Daftarkan Delegates"| MaterialApp["MaterialApp (Config)"]
MaterialApp -->|"Lookup via BuildContext"| Context["AppLocalizations.of(context)"]
Context -->|"Render Teks Dinamis / RTL"| UI["Widget UI (Text / Layout)"]
endSetup dan Konfigurasi Proyek #
Untuk memulai, kita perlu menambahkan dependensi pendukung lokalisasi dan mengaktifkan fitur pembuatan kode otomatis di dalam berkas konfigurasi proyek.
1. Modifikasi pubspec.yaml #
Tambahkan pustaka flutter_localizations (tersedia langsung di dalam SDK Flutter) dan pustaka intl untuk penanganan format angka, mata uang, dan tanggal tingkat lanjut. Jangan lupa untuk mengaktifkan flag generate: true di bagian konfigurasi flutter.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.20.1 # Pustaka format tanggal & pluralisasi
flutter:
generate: true # Mengizinkan Flutter melakukan code generation otomatis dari file ARB
2. Membuat File Konfigurasi l10n.yaml #
Buat berkas baru bernama l10n.yaml di root folder proyek kita (sejajar dengan pubspec.yaml). Berkas ini digunakan untuk mengonfigurasi bagaimana generator Flutter membuat kelas Dart dari berkas ARB kita.
# l10n.yaml
arb-dir: lib/l10n # Direktori tempat kita menyimpan berkas .arb
template-arb-file: app_id.arb # Berkas acuan/template bahasa utama (Bahasa Indonesia)
output-localization-file: app_localizations.dart
output-class: AppLocalizations # Nama kelas utama hasil generator
Application Resource Bundle (ARB) - Sintaksis & Struktur #
Berkas ARB adalah berkas JSON standar yang berisi pasangan kunci dan nilai terjemahan. Setiap kunci opsional dapat memiliki kunci metadata tambahan dengan awalan simbol @ yang berisi deskripsi konteks bagi penerjemah dan deklarasi variabel dinamis.
Kita akan membuat tiga file ARB di folder lib/l10n/ untuk mendukung bahasa Indonesia, Inggris, dan Arab.
1. Template Bahasa Indonesia (app_id.arb) #
// lib/l10n/app_id.arb
{
"@@locale": "id",
"appTitle": "Toko Buku Kita",
"@appTitle": {
"description": "Judul utama aplikasi yang tampil di app bar"
},
"welcomeMessage": "Selamat datang kembali, {username}!",
"@welcomeMessage": {
"description": "Pesan sambutan di halaman beranda",
"placeholders": {
"username": {
"type": "String",
"example": "Budi Santoso"
}
}
},
"notifikasiBukuTersedia": "{jumlahBuku, plural, =0{Buku belum tersedia} =1{Tersisa 1 buku terakhir!} other{Tersisa {jumlahBuku} buku lagi!}}",
"@notifikasiBukuTersedia": {
"description": "Status stok buku dengan pluralisasi",
"placeholders": {
"jumlahBuku": {
"type": "int",
"format": "compact"
}
}
},
"informasiGender": "{gender, select, male{Bapak {nama}} female{Ibu {nama}} other{Sdr. {nama}}}",
"@informasiGender": {
"description": "Menampilkan sapaan formal berdasarkan gender",
"placeholders": {
"gender": {
"type": "String"
},
"nama": {
"type": "String"
}
}
}
}
2. Terjemahan Bahasa Inggris (app_en.arb) #
// lib/l10n/app_en.arb
{
"@@locale": "en",
"appTitle": "Our Bookstore",
"welcomeMessage": "Welcome back, {username}!",
"notifikasiBukuTersedia": "{jumlahBuku, plural, =0{No books available} =1{Only 1 book left!} other{{jumlahBuku} books left!}}",
"informasiGender": "{gender, select, male{Mr. {nama}} female{Mrs. {nama}} other{{nama}}}"
}
3. Terjemahan Bahasa Arab (app_ar.arb) #
Dalam bahasa Arab, aturan pluralisasi (ICU Plural rules) jauh lebih kompleks dibandingkan bahasa Indonesia/Inggris karena membedakan kategori nol (zero), satu (one), dua (two), jamak sedikit (few), jamak banyak (many), dan lainnya (other).
// lib/l10n/app_ar.arb
{
"@@locale": "ar",
"appTitle": "متجر الكتب الخاص بنا",
"welcomeMessage": "مرحباً بك مجدداً، {username}!",
"notifikasiBukuTersedia": "{jumlahBuku, plural, =0{لا توجد كتب متاحة} =1{متبقي كتاب واحد فقط!} =2{متبقي كتابين اثنين!} few{متبقي {jumlahBuku} كتب!} many{متبقي {jumlahBuku} كتاباً!} other{متبقي {jumlahBuku} كتاب!}}",
"informasiGender": "{gender, select, male{السيد {nama}} female{السيدة {nama}} other{{nama}}}"
}
Setelah membuat berkas di atas, jalankan perintah generator di konsol terminal untuk membangun kelas Dart:
flutter gen-l10n
Proses ini juga berjalan secara otomatis setiap kali kita menjalankan flutter run atau flutter build.
Konfigurasi MaterialApp & Fallback Handling #
Setelah generator kode sukses memproduksi kelas AppLocalizations, kita harus mendaftarkan delegasi (delegates) dan daftar bahasa yang didukung ke dalam MaterialApp kita.
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Aplikasi Multi Bahasa',
// 1. Daftarkan delegasi lokalisasi bawaan Flutter & kelas kustom kita
localizationsDelegates: const [
AppLocalizations.delegate, // Kelas generated kita
GlobalMaterialLocalizations.delegate, // Untuk text internal widget Material
GlobalWidgetsLocalizations.delegate, // Menentukan arah teks (LTR/RTL)
GlobalCupertinoLocalizations.delegate, // Untuk komponen Cupertino (iOS)
],
// 2. Tentukan bahasa apa saja yang didukung oleh aplikasi kita
supportedLocales: const [
Locale('id'), // Bahasa Indonesia (Default)
Locale('en'), // Bahasa Inggris
Locale('ar'), // Bahasa Arab
],
// 3. Set locale aktif. Jika null, aplikasi akan otomatis mendeteksi bahasa perangkat
locale: const Locale('id'),
// 4. Fallback handler jika bahasa perangkat pengguna tidak didukung
localeResolutionCallback: (Locale? deviceLocale, Iterable<Locale> supportedLocales) {
if (deviceLocale == null) return const Locale('id');
for (final locale in supportedLocales) {
if (locale.languageCode == deviceLocale.languageCode) {
return locale; // Gunakan bahasa perangkat jika terdaftar
}
}
// Kembalikan ke Bahasa Indonesia sebagai default fallback
return const Locale('id');
},
home: const BerandaScreen(),
);
}
}
Menggunakan Terjemahan di Widget Tree #
Untuk mempermudah pemanggilan kode lokalisasi di dalam widget tree, kita dapat membuat metode ekstensi (extension method) pada BuildContext sehingga kita tidak perlu menuliskan AppLocalizations.of(context)! secara berulang kali.
// lib/core/extensions/l10n_extension.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension AppLocalizationsX on BuildContext {
/// Membuka shortcut pemanggilan data terjemahan
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
Berikut adalah contoh implementasi penggunaan terjemahan statik, dinamis, pluralisasi, dan seleksi bersyarat di halaman UI:
// lib/presentation/screens/beranda_screen.dart
import 'package:flutter/material.dart';
import '../../core/extensions/l10n_extension.dart';
class BerandaScreen extends StatelessWidget {
const BerandaScreen({super.key});
@override
Widget build(BuildContext context) {
// Kita memanfaatkan extension context.l10n untuk memotong boilerplate code
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.appTitle),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Teks dinamis dengan variabel String
Text(
context.l10n.welcomeMessage('Aditya Pratama'),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// 2. Teks dengan format pluralisasi kompleks
Text(context.l10n.notifikasiBukuTersedia(0)), // Output ID: "Buku belum tersedia"
Text(context.l10n.notifikasiBukuTersedia(1)), // Output ID: "Tersisa 1 buku terakhir!"
Text(context.l10n.notifikasiBukuTersedia(12)), // Output ID: "Tersisa 12 buku lagi!"
const SizedBox(height: 16),
// 3. Teks dengan logika select bersyarat
Text(context.l10n.informasiGender('male', 'Wibowo')), // Output: "Bapak Wibowo"
Text(context.l10n.informasiGender('female', 'Kartika')), // Output: "Ibu Kartika"
Text(context.l10n.informasiGender('other', 'Sanjaya')), // Output: "Sdr. Sanjaya"
],
),
),
);
}
}
Manajemen Locale Dinamis saat Runtime dengan Riverpod #
Di aplikasi produksi, pengguna sering kali ingin mengganti bahasa aplikasi secara manual melalui halaman pengaturan (in-app settings), terlepas dari apa pun bahasa sistem yang sedang aktif pada perangkat mereka. Kita akan membangun manajemen state locale dinamis yang menyimpan preferensi bahasa pengguna secara persisten menggunakan shared_preferences dan mengeksposnya menggunakan Riverpod.
1. Implementasi Preference Storage #
// lib/core/storage/preferences_helper.dart
import 'package:shared_preferences/shared_preferences.dart';
class PreferencesHelper {
static const String _keyLocale = 'app_locale';
final SharedPreferences _prefs;
PreferencesHelper(this._prefs);
/// Menyimpan preferensi kode bahasa aktif
Future<void> simpanLocale(String? languageCode) async {
if (languageCode == null) {
await _prefs.remove(_keyLocale);
} else {
await _prefs.setString(_keyLocale, languageCode);
}
}
/// Membaca preferensi kode bahasa tersimpan
String? ambilLocale() {
return _prefs.getString(_keyLocale);
}
}
2. State Notifier dengan Riverpod #
// lib/core/providers/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../storage/preferences_helper.dart';
// Provider global untuk SharedPreferences
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError('Harap override provider ini di main.dart');
});
// Provider untuk helper penyimpanan
final preferencesHelperProvider = Provider<PreferencesHelper>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return PreferencesHelper(prefs);
});
// Provider utama untuk memantau status Locale aktif
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale?>((ref) {
final helper = ref.watch(preferencesHelperProvider);
return LocaleNotifier(helper);
});
class LocaleNotifier extends StateNotifier<Locale?> {
final PreferencesHelper _helper;
LocaleNotifier(this._helper) : super(null) {
_muatLocaleTersimpan();
}
void _muatLocaleTersimpan() {
final kodeBahasa = _helper.ambilLocale();
if (kodeBahasa != null) {
state = Locale(kodeBahasa);
} else {
state = null; // null berarti otomatis mengikuti bahasa OS perangkat
}
}
/// Mengganti bahasa aplikasi
Future<void> ubahLocale(String languageCode) async {
await _helper.simpanLocale(languageCode);
state = Locale(languageCode);
}
/// Mereset kembali untuk mengikuti bahasa default sistem operasi
Future<void> resetKeBahasaSistem() async {
await _helper.simpanLocale(null);
state = null;
}
}
3. Integrasi Dropdown Pemilih Bahasa di UI #
// lib/presentation/widgets/pemilih_bahasa_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/locale_provider.dart';
class PemilihBahasaWidget extends ConsumerWidget {
const PemilihBahasaWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final Locale? localeAktif = ref.watch(localeProvider);
return DropdownButton<String>(
value: localeAktif?.languageCode ?? 'id',
icon: const Icon(Icons.language),
underline: Container(height: 2, color: Colors.blueAccent),
onChanged: (String? kodeBaru) {
if (kodeBaru != null) {
ref.read(localeProvider.notifier).ubahLocale(kodeBaru);
}
},
items: const [
DropdownMenuItem(
value: 'id',
child: Text('🇮🇩 Bahasa Indonesia'),
),
DropdownMenuItem(
value: 'en',
child: Text('🇬🇧 English'),
),
DropdownMenuItem(
value: 'ar',
child: Text('🇸🇦 العربية (RTL)'),
),
],
);
}
}
Dukungan Tata Letak Right-to-Left (RTL) #
Arah pembacaan teks tidak selalu dari kiri ke kanan (Left-to-Right / LTR). Beberapa bahasa besar dunia seperti bahasa Arab (ar), Ibrani (he), dan Persia (fa) menggunakan arah penulisan dari kanan ke kiri (Right-to-Left / RTL).
Flutter memiliki keunggulan arsitektural yang luar biasa dalam menangani RTL secara otomatis. Ketika aplikasi mendeteksi locale aktif diubah menjadi bahasa Arab, seluruh sumbu koordinat horizontal dan tata letak reorderable akan dibalik arahnya secara otomatis oleh engine render Flutter. Namun, sebagai developer, kita harus mematuhi aturan penulisan widget agar tidak merusak tata letak simetris tersebut.
1. Gunakan Koordinat Logis Alih-alih Koordinat Absolut #
Jangan pernah menggunakan nilai padding atau margin bertipe absolut (left dan right) untuk komponen dinamis. Gunakan sumbu koordinat logis (start dan end).
// ANTI-PATTERN: Padding tidak akan berubah posisi saat tata letak dibalik ke RTL
Padding(
padding: const EdgeInsets.only(left: 16.0, right: 8.0),
child: const Text('Indeks Konten'),
)
// BENAR: start berarti kiri pada LTR, dan otomatis menjadi kanan pada RTL
Padding(
padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0),
child: const Text('Indeks Konten'),
)
Aturan koordinat logis ini juga berlaku untuk properti alignment dan border radius:
- Gunakan
AlignmentDirectional.centerStartalih-alihAlignment.centerLeft. - Gunakan
BorderRadiusDirectional.only(topStart: Radius.circular(8))alih-alihBorderRadius.only(topLeft: Radius.circular(8)).
2. Penanganan Arah Ikon (RTL-aware Icons) #
Beberapa ikon petunjuk navigasi (seperti tanda panah kembali atau panah maju) harus dibalik arahnya di mode RTL karena pengguna membaca dari kanan ke kiri. Kita harus menggunakan ikon bawaan Flutter yang memiliki adaptasi arah otomatis:
// Gunakan ikon berakhiran _directional agar otomatis membalik arah saat RTL aktif
Icon(Icons.arrow_back_ios_new_outlined) // Secara otomatis menjadi panah ke kanan di RTL
// Untuk ikon statis yang menggambarkan objek (seperti kamera, settings, share),
// arahnya tetap sama dan tidak perlu dibalik.
Format Angka, Mata Uang, dan Tanggal dengan Library intl #
Masing-masing daerah memiliki format tampilan angka desimal, simbol mata uang, dan penamaan bulan tanggal yang khas. Paket intl menyediakan API bertipe aman untuk menstandarisasikan tampilan ini sesuai locale pengguna.
Berikut adalah kelas pembantu (helper class) untuk mempermudah format data ini:
// lib/core/utils/format_helper.dart
import 'package:intl/intl.dart';
class FormatHelper {
/// Format angka reguler (desimal) sesuai locale aktif
/// id: 1.250.000,75
/// en: 1,250,000.75
static String formatAngka(double nilai, String locale) {
return NumberFormat.decimalPattern(locale).format(nilai);
}
/// Format mata uang terstandarisasi regional
/// id: Rp150.000
/// en: $10.00
static String formatMataUang(double nilai, {required String kodeMataUang, required String locale}) {
return NumberFormat.simpleCurrency(
locale: locale,
name: kodeMataUang,
decimalDigits: kodeMataUang == 'IDR' ? 0 : 2,
).format(nilai);
}
/// Format tanggal panjang
/// id: 16 Juni 2026
/// en: June 16, 2026
/// ar: ١٦ يونيو ٢٠٢٦
static String formatTanggalPanjang(DateTime tanggal, String locale) {
return DateFormat.yMMMMd(locale).format(tanggal);
}
/// Mengonversi waktu relatif (human readable)
/// Output contoh: "3 menit yang lalu" atau "sebelumnya"
static String formatWaktuRelatif(DateTime waktuTarget, String locale) {
final DateTime sekarang = DateTime.now();
final Duration selisih = sekarang.difference(waktuTarget);
if (selisih.inMinutes < 1) {
return locale == 'id' ? 'Baru saja' : 'Just now';
} else if (selisih.inHours < 1) {
return locale == 'id'
? '${selisih.inMinutes} menit yang lalu'
: '${selisih.inMinutes} minutes ago';
} else if (selisih.inDays < 1) {
return locale == 'id'
? '${selisih.inHours} jam yang lalu'
: '${selisih.inHours} hours ago';
} else {
return formatTanggalPanjang(waktuTarget, locale);
}
}
}
Ringkasan #
- Keamanan Tipe (Type Safety): Manfaatkan generator resmi
flutter gen-l10nuntuk mengubah berkas ARB JSON menjadi kelas DartAppLocalizationsbertipe aman guna mencegah kesalahan pengetikan key string.- ICU Pluralisasi: Gunakan format pluralisasi ICU di dalam file ARB untuk menangani perbedaan kategori jumlah kata benda, terutama pada bahasa Arab yang memiliki 6 variasi bentuk jamak.
- Penggantian Bahasa Runtime: Simpan preferensi bahasa pengguna secara persisten di
SharedPreferencesdan sebar lokasinya keMaterialAppmelalui Riverpod untuk mendukung perubahan bahasa instan tanpa restart aplikasi.- Prinsip Desain RTL: Selalu gunakan properti arah logis seperti
EdgeInsetsDirectionaldanAlignmentDirectionalagar tata letak aplikasi beradaptasi secara dinamis saat pengguna mengganti bahasa ke mode penulisan kanan-ke-kiri.- Standardisasi intl: Gunakan pustaka
intluntuk melakukan pemformatan visual desimal, mata uang, dan tanggal secara otomatis berdasarkan representasi locale lokal.
← Sebelumnya: Push Notification & Deep Link Berikutnya: Accessibility →