Internationalization #
Internationalization (i18n) adalah proses menyiapkan aplikasi untuk mendukung berbagai bahasa dan wilayah. Flutter memiliki dukungan i18n yang sangat baik melalui package resmi flutter_localizations dan code generation dari file ARB (Application Resource Bundle). Jika kamu berencana merilis app di lebih dari satu bahasa sejak awal, menyiapkan i18n dari awal jauh lebih mudah daripada menambahkannya belakangan.
Setup #
# pubspec.yaml
dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.20.1 # format tanggal, angka, pluralisasi
flutter:
generate: true # aktifkan code generation dari ARB
# Opsional: path ARB kustom (default: lib/l10n)
# l10n.yaml -- konfigurasi code generation
arb-dir: lib/l10n
template-arb-file: app_id.arb # bahasa default (template)
output-localization-file: app_localizations.dart
output-class: AppLocalizations
# untranslated-messages-file: untranslated_messages.txt # log terjemahan yang hilang
File ARB — Definisi String #
File ARB (Application Resource Bundle) adalah file JSON dengan metadata terjemahan:
// lib/l10n/app_id.arb -- TEMPLATE (Bahasa Indonesia, bahasa default)
{
"@@locale": "id",
"appTitle": "Toko Saya",
"@appTitle": {
"description": "Nama aplikasi"
},
"welcomeMessage": "Selamat datang, {name}!",
"@welcomeMessage": {
"description": "Pesan sambutan pengguna",
"placeholders": {
"name": {
"type": "String",
"example": "Budi"
}
}
},
"itemCount": "{count, plural, =0{Tidak ada item} =1{1 item} other{{count} item}}",
"@itemCount": {
"description": "Jumlah item dengan pluralisasi",
"placeholders": {
"count": {
"type": "num",
"format": "compact"
}
}
},
"harga": "Rp {amount}",
"@harga": {
"description": "Format harga",
"placeholders": {
"amount": {
"type": "double",
"format": "decimalPattern",
"optionalParameters": {
"decimalDigits": 0
}
}
}
},
"loginButton": "Masuk",
"@loginButton": { "description": "Teks tombol login" },
"errorNetwork": "Tidak ada koneksi internet. Periksa jaringan kamu.",
"@errorNetwork": { "description": "Pesan error jaringan" },
"lastUpdated": "Diperbarui: {date}",
"@lastUpdated": {
"description": "Waktu terakhir diperbarui",
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
}
}
// lib/l10n/app_en.arb -- Terjemahan Inggris
{
"@@locale": "en",
"appTitle": "My Store",
"welcomeMessage": "Welcome, {name}!",
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"harga": "IDR {amount}",
"loginButton": "Sign In",
"errorNetwork": "No internet connection. Check your network.",
"lastUpdated": "Updated: {date}"
}
// lib/l10n/app_ar.arb -- Terjemahan Arab (RTL)
{
"@@locale": "ar",
"appTitle": "متجري",
"welcomeMessage": "مرحباً، {name}!",
"itemCount": "{count, plural, =0{لا توجد عناصر} =1{عنصر واحد} two{عنصران} few{{count} عناصر} other{{count} عنصر}}",
"loginButton": "تسجيل الدخول",
"errorNetwork": "لا يوجد اتصال بالإنترنت. تحقق من شبكتك."
}
# Generate kode dari file ARB
flutter gen-l10n
# atau otomatis saat flutter run / flutter build
Konfigurasi MaterialApp #
// main.dart
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeProvider); // bahasa aktif dari provider
return MaterialApp(
// Konfigurasi lokalisasi
localizationsDelegates: const [
AppLocalizations.delegate, // delegate generated kita
GlobalMaterialLocalizations.delegate, // Material widget (DatePicker, dll)
GlobalWidgetsLocalizations.delegate, // widget dasar (teks arah)
GlobalCupertinoLocalizations.delegate, // Cupertino widget
],
supportedLocales: const [
Locale('id'), // Bahasa Indonesia
Locale('en'), // English
Locale('ar'), // Arabic
Locale('zh'), // Chinese (Simplified)
],
locale: locale, // null = ikuti bahasa device
// Fallback jika bahasa device tidak didukung
localeResolutionCallback: (deviceLocale, supportedLocales) {
if (deviceLocale == null) return const Locale('id');
// Cari locale yang cocok
for (final supported in supportedLocales) {
if (supported.languageCode == deviceLocale.languageCode) {
return supported;
}
}
// Default ke Indonesia jika tidak ada yang cocok
return const Locale('id');
},
home: const HomeScreen(),
);
}
}
Menggunakan Terjemahan di Widget #
// Setelah generate, gunakan AppLocalizations.of(context)
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; // shortcut
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Column(
children: [
// String sederhana
Text(l10n.loginButton),
// String dengan placeholder
Text(l10n.welcomeMessage('Budi')),
// Pluralisasi -- otomatis memilih bentuk yang tepat
Text(l10n.itemCount(0)), // "Tidak ada item"
Text(l10n.itemCount(1)), // "1 item"
Text(l10n.itemCount(42)), // "42 item"
// Format harga
Text(l10n.harga(150000)), // "Rp 150.000"
// Format tanggal
Text(l10n.lastUpdated(DateTime.now())), // "Diperbarui: 15 Jan 2025"
],
),
);
}
}
// Extension untuk akses lebih pendek
extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
// Penggunaan dengan extension
Text(context.l10n.appTitle)
Text(context.l10n.welcomeMessage('Budi'))
Ganti Bahasa Runtime dengan Riverpod #
// lib/core/providers/locale_provider.dart
final localeProvider = NotifierProvider<LocaleNotifier, Locale?>(
LocaleNotifier.new,
);
class LocaleNotifier extends Notifier<Locale?> {
@override
Locale? build() {
// Baca bahasa tersimpan dari SharedPreferences
final prefs = ref.watch(appPreferencesProvider);
final saved = prefs.bahasa;
if (saved == null) return null; // null = ikuti device
return Locale(saved);
}
Future<void> setLocale(String languageCode) async {
final prefs = ref.read(appPreferencesProvider);
await prefs.setBahasa(languageCode);
state = Locale(languageCode);
}
Future<void> resetToDevice() async {
final prefs = ref.read(appPreferencesProvider);
await prefs.setBahasa(null);
state = null;
}
}
// Widget pemilih bahasa
class LanguageSelectorWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = ref.watch(localeProvider);
return DropdownButton<String>(
value: currentLocale?.languageCode ?? 'id',
items: const [
DropdownMenuItem(value: 'id', child: Text('🇮🇩 Bahasa Indonesia')),
DropdownMenuItem(value: 'en', child: Text('🇬🇧 English')),
DropdownMenuItem(value: 'ar', child: Text('🇸🇦 العربية')),
],
onChanged: (code) {
if (code != null) {
ref.read(localeProvider.notifier).setLocale(code);
}
},
);
}
}
Dukungan RTL (Right-to-Left) #
Bahasa seperti Arab dan Ibrani ditulis dari kanan ke kiri. Flutter mendukung RTL secara otomatis jika locale yang diset adalah RTL:
// Flutter otomatis membalik layout untuk RTL
// Tapi perlu pastikan widget kamu RTL-aware:
// AMAN: gunakan Start/End alih-alih Left/Right
Padding(
padding: const EdgeInsets.only(
start: 16, // kiri di LTR, kanan di RTL
end: 8, // kanan di LTR, kiri di RTL
),
child: text,
)
// Row dengan MainAxisAlignment -- otomatis dibalik
Row(
children: [
const Icon(Icons.arrow_back), // menjadi arrow_forward di RTL!
// Gunakan Icons.arrow_back_ios_new yang aware RTL
// atau tentukan secara eksplisit:
],
)
// Gunakan Directionality.of(context) untuk cek arah saat ini
final isRTL = Directionality.of(context) == TextDirection.rtl;
// Atau paksa RTL untuk widget tertentu
Directionality(
textDirection: TextDirection.rtl,
child: MyWidget(),
)
// Icons yang RTL-aware
Icon(Icons.arrow_back) // otomatis dibalik di RTL
// Gunakan: Icons.arrow_back_ios vs Icons.arrow_forward_ios sesuai konteks
Format Angka dan Tanggal per Locale #
import 'package:intl/intl.dart';
class FormatHelper {
// Format angka sesuai locale
static String formatAngka(double angka, String locale) {
return NumberFormat.decimalPattern(locale).format(angka);
// id: 1.234.567,89
// en: 1,234,567.89
}
// Format mata uang
static String formatMataUang(double angka, {String currency = 'IDR', String locale = 'id'}) {
return NumberFormat.currency(
locale: locale,
symbol: currency == 'IDR' ? 'Rp ' : '\$',
decimalDigits: currency == 'IDR' ? 0 : 2,
).format(angka);
// IDR: Rp 150.000
// USD: $1,500.00
}
// Format tanggal
static String formatTanggal(DateTime tanggal, String locale) {
return DateFormat.yMMMMd(locale).format(tanggal);
// id: 15 Januari 2025
// en: January 15, 2025
// ar: ١٥ يناير ٢٠٢٥
}
// Format relatif (X menit lalu, kemarin, dll.)
static String formatRelatif(DateTime tanggal, BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final selisih = DateTime.now().difference(tanggal);
if (selisih.inMinutes < 1) return l10n.justNow;
if (selisih.inHours < 1) return l10n.minutesAgo(selisih.inMinutes);
if (selisih.inDays < 1) return l10n.hoursAgo(selisih.inHours);
if (selisih.inDays < 7) return l10n.daysAgo(selisih.inDays);
return formatTanggal(tanggal, Localizations.localeOf(context).languageCode);
}
}
Ringkasan #
- Setup i18n dengan
flutter_localizationsdan file ARB — aktifkangenerate: truedi pubspec.yaml untuk code generation otomatis.- File ARB template (
app_id.arb) mendefinisikan semua key dan metadata. File terjemahan (app_en.arb, dll.) hanya perlu menyertakan nilai terjemahannya.- Gunakan placeholder untuk string dinamis (
{name}), format bawaan untuk angka dan tanggal (format: "decimalPattern"), dan plural untuk string yang bergantung pada jumlah.- Buat extension
context.l10nuntuk akses terjemahan yang lebih pendek dan ergonomis dari mana saja.- Ganti bahasa saat runtime dengan menyimpan locale di
SharedPreferencesdan expose via Riverpod provider —MaterialAppakan otomatis re-render saat locale berubah.- Flutter mendukung RTL secara otomatis — gunakan
EdgeInsets.only(start:, end:)alih-alihleft/right, danDirectionality.of(context)untuk deteksi arah teks saat ini.- Gunakan
intlpackage untuk format angka, mata uang, dan tanggal yang sesuai locale —NumberFormat.currency(),DateFormat.yMMMMd(locale).
← Sebelumnya: Push Notification & Deep Link Berikutnya: Accessibility →