Flutter 튜토리얼 39편: 다국어 지원(i18n)
Flutter 다국어 지원(i18n)
Flutter 앱에 다국어 지원을 추가하는 방법을 알아봅니다. 사용자의 언어 설정에 따라 적절한 번역을 표시하고, 날짜/시간/숫자 형식을 지역화하는 방법을 살펴보겠습니다.
국제화와 지역화의 이해
Why - 왜 다국어 지원이 필요한가요?
앱을 전 세계 사용자에게 제공하려면 각 사용자의 언어와 문화에 맞는 경험을 제공해야 합니다. 영어만 지원하는 앱은 비영어권 사용자에게 진입 장벽이 됩니다. 다국어 지원은 사용자층을 넓히고 앱의 접근성을 높이는 핵심 기능입니다.
What - 국제화(i18n)와 지역화(l10n)란 무엇인가요?
국제화(Internationalization, i18n)1는 앱이 여러 언어와 지역을 지원할 수 있도록 설계하는 과정입니다. 텍스트를 코드에서 분리하고, 날짜/숫자 형식을 유연하게 처리할 수 있는 구조를 만듭니다.
지역화(Localization, l10n)2는 특정 언어와 지역에 맞게 콘텐츠를 번역하고 적용하는 과정입니다. 한국어 번역 파일을 추가하거나, 한국식 날짜 형식을 적용하는 것이 지역화에 해당합니다.
How - Flutter에서는 어떻게 구현하나요?
Flutter는 flutter_localizations 패키지와 intl 패키지를 통해 국제화를 지원합니다.
┌─────────────────────────────────────────────┐│ ARB 파일 (번역 원본) ││ app_ko.arb, app_en.arb, app_ja.arb │└─────────────────┬───────────────────────────┘ │ flutter gen-l10n ▼┌─────────────────────────────────────────────┐│ 생성된 Dart 코드 ││ AppLocalizations 클래스 │└─────────────────┬───────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ 앱에서 사용 ││ AppLocalizations.of(context)!.hello │└─────────────────────────────────────────────┘Watch out - 주의사항
지역화는 단순 번역이 아닙니다. 문화적 차이(날짜 형식, 통화 기호, 읽기 방향 등)도 고려해야 합니다.
프로젝트 설정
Why - 왜 별도 패키지가 필요한가요?
Flutter 핵심 프레임워크는 경량화를 위해 지역화 데이터를 포함하지 않습니다. Material 위젯의 번역(예: DatePicker의 “취소”, “확인” 버튼)과 날짜/숫자 형식 데이터는 별도 패키지로 제공됩니다.
What - 어떤 패키지를 설치하나요?
pubspec.yaml에 다음 의존성을 추가합니다:
dependencies: flutter: sdk: flutter flutter_localizations: # Flutter 위젯 번역 sdk: flutter intl: any # 날짜/숫자 형식화
flutter: generate: true # l10n 코드 자동 생성 활성화How - 설정 파일을 어떻게 구성하나요?
프로젝트 루트에 l10n.yaml 파일을 생성합니다:
arb-dir: lib/l10ntemplate-arb-file: app_en.arboutput-localization-file: app_localizations.dart| 옵션 | 설명 |
|---|---|
arb-dir | ARB 파일이 위치한 디렉토리 |
template-arb-file | 기본 템플릿 ARB 파일 (보통 영어) |
output-localization-file | 생성될 Dart 파일 이름 |
Watch out - 주의사항
flutter: generate: true 설정을 빠뜨리면 코드가 자동 생성되지 않습니다.
빌드 시 오류가 발생하면 이 설정을 먼저 확인하세요.
ARB 파일 작성
Why - 왜 ARB 형식을 사용하나요?
ARB(Application Resource Bundle)3는 ICU 메시지 형식을 기반으로 한 JSON 확장 형식입니다. 복수형, 성별, 변수 치환 등 복잡한 번역 패턴을 지원하면서도 번역 도구와의 호환성이 좋습니다.
What - ARB 파일의 구조는 어떤가요?
lib/l10n/app_en.arb (템플릿 파일):
{ "@@locale": "en", "appTitle": "My App", "@appTitle": { "description": "The title of the application" }, "hello": "Hello {name}!", "@hello": { "description": "Greeting message", "placeholders": { "name": { "type": "String", "example": "John" } } }, "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}", "@itemCount": { "description": "Shows number of items", "placeholders": { "count": { "type": "int" } } }}lib/l10n/app_ko.arb (한국어 번역):
{ "@@locale": "ko", "appTitle": "내 앱", "hello": "안녕하세요, {name}님!", "itemCount": "{count, plural, =0{항목 없음} other{{count}개 항목}}"}How - 메시지 형식을 어떻게 작성하나요?
변수 치환
"greeting": "Hello, {userName}!"복수형 처리
"messages": "{count, plural, =0{No messages} =1{1 message} other{{count} messages}}"성별 처리
"pronoun": "{gender, select, male{He} female{She} other{They}}"날짜 형식
"lastLogin": "Last login: {date}","@lastLogin": { "placeholders": { "date": { "type": "DateTime", "format": "yMd" } }}Watch out - 주의사항
@접두사가 붙은 키는 메타데이터용입니다@@locale은 해당 파일의 로케일을 지정합니다- 복수형 규칙은 언어마다 다릅니다 (한국어는 단수/복수 구분이 덜 엄격함)
MaterialApp 구성
Why - 왜 MaterialApp에 설정이 필요한가요?
Flutter는 앱 시작 시 지원 언어 목록과 현재 로케일을 알아야 합니다. MaterialApp에 이 정보를 제공해야 적절한 번역을 로드하고 Material 위젯도 해당 언어로 표시됩니다.
What - 어떤 속성을 설정하나요?
import 'package:flutter/material.dart';import 'package:flutter_localizations/flutter_localizations.dart';import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() { runApp(const MyApp());}
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return MaterialApp( title: 'My App', // 지원하는 로케일 목록 supportedLocales: AppLocalizations.supportedLocales, // 지역화 델리게이트 목록 localizationsDelegates: AppLocalizations.localizationsDelegates, home: const HomePage(), ); }}How - 델리게이트의 역할은 무엇인가요?
AppLocalizations.localizationsDelegates는 다음 델리게이트를 포함합니다:
| 델리게이트 | 역할 |
|---|---|
AppLocalizations.delegate | 앱 번역 문자열 제공 |
GlobalMaterialLocalizations.delegate | Material 위젯 번역 |
GlobalWidgetsLocalizations.delegate | 기본 위젯 방향(RTL/LTR) |
GlobalCupertinoLocalizations.delegate | Cupertino 위젯 번역 |
Watch out - 주의사항
코드 생성이 완료되기 전에는 AppLocalizations를 import할 수 없습니다.
flutter pub get 또는 flutter run을 실행하면 자동으로 생성됩니다.
생성된 파일 위치: .dart_tool/flutter_gen/gen_l10n/app_localizations.dart
번역 문자열 사용
Why - 왜 하드코딩된 문자열을 피해야 하나요?
코드에 직접 작성된 문자열은 번역할 수 없습니다. 모든 사용자 대면 텍스트는 지역화 시스템을 통해 제공해야 여러 언어를 지원할 수 있습니다.
What - 어떻게 번역 문자열에 접근하나요?
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HomePage extends StatelessWidget { const HomePage({super.key});
@override Widget build(BuildContext context) { // 현재 로케일에 맞는 번역 객체 가져오기 final l10n = AppLocalizations.of(context)!;
return Scaffold( appBar: AppBar( title: Text(l10n.appTitle), // "내 앱" (한국어) ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 변수 치환 Text(l10n.hello('홍길동')), // "안녕하세요, 홍길동님!"
// 복수형 Text(l10n.itemCount(0)), // "항목 없음" Text(l10n.itemCount(1)), // "1개 항목" Text(l10n.itemCount(5)), // "5개 항목" ], ), ), ); }}How - Extension을 활용한 간결한 접근
extension BuildContextL10n on BuildContext { AppLocalizations get l10n => AppLocalizations.of(this)!;}
// 사용class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Text(context.l10n.hello('Flutter')); }}Watch out - 주의사항
AppLocalizations.of(context)는 null을 반환할 수 있습니다.
MaterialApp 아래에서만 사용 가능하며, 지원하지 않는 로케일인 경우 null입니다.
! 연산자 사용 시 이 점을 인지하세요.
로케일 변경
Why - 왜 수동 로케일 변경이 필요한가요?
기본적으로 시스템 설정을 따르지만, 앱 내에서 언어를 선택할 수 있는 기능을 제공하고 싶을 때가 있습니다. 사용자가 시스템 설정과 다른 언어로 앱을 사용하고 싶은 경우를 지원합니다.
What - 어떻게 로케일을 변경하나요?
class MyApp extends StatefulWidget { const MyApp({super.key});
// 전역 키를 통한 상태 접근 (간단한 예시) static void setLocale(BuildContext context, Locale locale) { final state = context.findAncestorStateOfType<_MyAppState>(); state?.setLocale(locale); }
@override State<MyApp> createState() => _MyAppState();}
class _MyAppState extends State<MyApp> { Locale? _locale;
void setLocale(Locale locale) { setState(() { _locale = locale; }); }
@override Widget build(BuildContext context) { return MaterialApp( locale: _locale, // null이면 시스템 설정 사용 supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: const HomePage(), ); }}How - 언어 선택 UI 구현
class LanguageSelector extends StatelessWidget { const LanguageSelector({super.key});
@override Widget build(BuildContext context) { return DropdownButton<Locale>( value: Localizations.localeOf(context), items: const [ DropdownMenuItem( value: Locale('ko'), child: Text('한국어'), ), DropdownMenuItem( value: Locale('en'), child: Text('English'), ), DropdownMenuItem( value: Locale('ja'), child: Text('日本語'), ), ], onChanged: (locale) { if (locale != null) { MyApp.setLocale(context, locale); } }, ); }}Watch out - 주의사항
로케일 변경 시 앱 전체가 리빌드됩니다. 상태 관리 솔루션(Provider, Riverpod 등)을 사용하면 더 깔끔하게 구현할 수 있습니다.
날짜와 숫자 형식화
Why - 왜 형식화가 중요한가요?
같은 날짜도 지역마다 표기 방식이 다릅니다:
- 한국: 2024년 1월 15일
- 미국: January 15, 2024
- 영국: 15 January 2024
숫자와 통화도 마찬가지입니다. 지역에 맞는 형식을 사용해야 사용자가 혼란 없이 정보를 이해할 수 있습니다.
What - intl 패키지로 어떻게 형식화하나요?
import 'package:intl/intl.dart';
class FormattingExample extends StatelessWidget { const FormattingExample({super.key});
@override Widget build(BuildContext context) { final locale = Localizations.localeOf(context).toString(); final now = DateTime.now(); final price = 1234567.89;
return Column( children: [ // 날짜 형식화 Text(DateFormat.yMMMMd(locale).format(now)), // ko: 2024년 1월 15일 // en: January 15, 2024
// 시간 형식화 Text(DateFormat.jm(locale).format(now)), // ko: 오후 3:30 // en: 3:30 PM
// 숫자 형식화 Text(NumberFormat.decimalPattern(locale).format(price)), // ko: 1,234,567.89 // de: 1.234.567,89
// 통화 형식화 Text(NumberFormat.currency(locale: locale, symbol: '₩').format(price)), // ₩1,234,568 ], ); }}How - 자주 사용하는 DateFormat 패턴
| 패턴 | 예시 (한국어) | 설명 |
|---|---|---|
yMd() | 2024. 1. 15. | 년월일 |
yMMMMd() | 2024년 1월 15일 | 년월일 (전체) |
yMMMEd() | 2024년 1월 15일 월 | 년월일요일 |
jm() | 오후 3<30>30> | 시간 |
Hm() | 15<30>30> | 24시간제 |
Watch out - 주의사항
intl 패키지 사용 전 initializeDateFormatting()을 호출해야 합니다:
import 'package:intl/date_symbol_data_local.dart';
void main() async { WidgetsFlutterBinding.ensureInitialized(); await initializeDateFormatting(); // 모든 로케일 초기화 runApp(const MyApp());}실전 예제: 다국어 쇼핑 앱
{ "@@locale": "en", "cartTitle": "Shopping Cart", "cartEmpty": "Your cart is empty", "cartItems": "{count, plural, =0{No items} =1{1 item} other{{count} items}}", "productPrice": "{price}", "@productPrice": { "placeholders": { "price": { "type": "double", "format": "currency", "optionalParameters": { "symbol": "$" } } } }, "checkoutButton": "Checkout", "lastUpdated": "Last updated: {date}", "@lastUpdated": { "placeholders": { "date": { "type": "DateTime", "format": "yMMMd" } } }}
// lib/l10n/app_ko.arb{ "@@locale": "ko", "cartTitle": "장바구니", "cartEmpty": "장바구니가 비어있습니다", "cartItems": "{count, plural, =0{상품 없음} other{상품 {count}개}}", "productPrice": "{price}", "@productPrice": { "placeholders": { "price": { "type": "double", "format": "currency", "optionalParameters": { "symbol": "₩", "decimalDigits": 0 } } } }, "checkoutButton": "결제하기", "lastUpdated": "최종 업데이트: {date}"}// 사용 예시class CartScreen extends StatelessWidget { final List<CartItem> items; final DateTime lastUpdate;
const CartScreen({ required this.items, required this.lastUpdate, super.key, });
@override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!;
return Scaffold( appBar: AppBar( title: Text(l10n.cartTitle), ), body: items.isEmpty ? Center(child: Text(l10n.cartEmpty)) : ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return ListTile( title: Text(item.name), trailing: Text(l10n.productPrice(item.price)), ); }, ), bottomNavigationBar: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(l10n.cartItems(items.length)), Text(l10n.lastUpdated(lastUpdate)), const SizedBox(height: 8), ElevatedButton( onPressed: items.isEmpty ? null : () {}, child: Text(l10n.checkoutButton), ), ], ), ), ); }}마무리
Flutter 다국어 지원의 핵심 개념을 정리하면:
- 패키지 설정:
flutter_localizations,intl의존성 추가 - ARB 파일 작성: 각 언어별 번역 파일 생성
- 코드 생성:
flutter gen-l10n또는 자동 생성 - MaterialApp 구성: 델리게이트와 지원 로케일 설정
- 번역 사용:
AppLocalizations.of(context)활용 - 형식화:
intl패키지로 날짜/숫자/통화 지역화
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!