Flutter 앱을 국제화(i18n)하는 방법은 무엇인가요?
질문
Flutter 애플리케이션을 여러 언어로 국제화(internationalization, i18n)하는 방법과 모범 사례에 대해 설명해주세요.
답변
Flutter 앱을 국제화하면 다양한 지역과 언어의 사용자들에게 현지화된 경험을 제공할 수 있습니다. Flutter는 앱 국제화를 위한 강력한 도구와 패턴을 제공합니다. 아래에서 Flutter 앱을 효과적으로 국제화하는 방법과 모범 사례를 살펴보겠습니다.
1. 기본 국제화 설정
1.1 필요한 패키지 추가
Flutter 앱을 국제화하기 위해서는 flutter_localizations
패키지와 intl
패키지를 사용합니다:
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.18.0
flutter:
generate: true # 자동 생성된 국제화 파일을 위한 설정
1.2 국제화 대리자(Delegate) 설정
MaterialApp
에 로컬라이제이션 대리자와 지원 언어를 설정합니다:
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 국제화 데모',
// 지원하는 언어 목록
supportedLocales: const [
Locale('en', ''), // 영어
Locale('ko', ''), // 한국어
Locale('ja', ''), // 일본어
Locale('zh', ''), // 중국어
],
// 로컬라이제이션 대리자 설정
localizationsDelegates: const [
AppLocalizations.delegate, // 앱 특화 로컬라이제이션
GlobalMaterialLocalizations.delegate, // Material 위젯 로컬라이제이션
GlobalWidgetsLocalizations.delegate, // 기본 위젯 로컬라이제이션
GlobalCupertinoLocalizations.delegate, // Cupertino 위젯 로컬라이제이션
],
// 현재 로케일을 결정하는 콜백
localeResolutionCallback: (locale, supportedLocales) {
// 기기 로케일이 지원되는지 확인
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale?.languageCode) {
return supportedLocale;
}
}
// 기본 로케일(영어) 반환
return supportedLocales.first;
},
home: MyHomePage(),
);
}
}
2. ARB 파일을 사용한 번역 관리
2.1 ARB 파일 생성
lib/l10n
디렉토리를 생성하고 언어별 ARB(Application Resource Bundle) 파일을 추가합니다:
lib/
└── l10n/
├── app_en.arb # 영어
├── app_ko.arb # 한국어
├── app_ja.arb # 일본어
└── app_zh.arb # 중국어
영어 ARB 파일의 예시(app_en.arb
):
{
"helloWorld": "Hello World",
"@helloWorld": {
"description": "The conventional greeting"
},
"welcome": "Welcome {username}",
"@welcome": {
"description": "Welcome message with username",
"placeholders": {
"username": {
"type": "String",
"example": "John"
}
}
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "The number of items",
"placeholders": {
"count": {
"type": "int",
"format": "compact"
}
}
}
}
한국어 ARB 파일의 예시(app_ko.arb
):
{
"helloWorld": "안녕하세요",
"welcome": "{username}님 환영합니다",
"itemCount": "{count, plural, =0{항목 없음} =1{1개 항목} other{{count}개 항목}}"
}
2.2 l10n.yaml 설정
프로젝트 루트에 l10n.yaml
파일을 생성하여 국제화 생성 설정을 지정합니다:
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
3. 번역된 문자열 사용
3.1 기본 사용법
번역된 문자열에 접근하는 방법:
// 위젯에서 번역된 문자열 사용
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// AppLocalizations 인스턴스 가져오기
final localizations = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
// 번역된 문자열 사용
title: Text(localizations.helloWorld),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 매개변수가 있는 번역 문자열
Text(localizations.welcome('사용자')),
// 복수형 처리
Text(localizations.itemCount(5)),
],
),
),
);
}
}
3.2 확장 메서드 사용
편의성을 위해 BuildContext
확장 메서드를 정의할 수 있습니다:
// extensions/context_extension.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension LocalizationsExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
// 사용 예시
Text(context.l10n.helloWorld)
4. 로케일 변경 구현
4.1 Provider를 사용한 로케일 관리
사용자가 앱 내에서 언어를 변경할 수 있도록 Provider
패턴을 사용할 수 있습니다:
// providers/locale_provider.dart
import 'package:flutter/material.dart';
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('en', '');
Locale get locale => _locale;
void setLocale(Locale locale) {
_locale = locale;
notifyListeners();
}
}
// main.dart
import 'package:provider/provider.dart';
import 'providers/locale_provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => LocaleProvider(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<LocaleProvider>(
builder: (context, localeProvider, child) {
return MaterialApp(
locale: localeProvider.locale, // 현재 설정된 로케일 사용
supportedLocales: const [
Locale('en', ''),
Locale('ko', ''),
// 기타 지원 언어...
],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: MyHomePage(),
);
},
);
}
}
4.2 언어 선택 UI 구현
// 언어 선택 화면
class LanguageSelectionScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localeProvider = Provider.of<LocaleProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.languageSettings),
),
body: ListView(
children: [
_buildLanguageItem(
context,
'영어',
const Locale('en', ''),
localeProvider
),
_buildLanguageItem(
context,
'한국어',
const Locale('ko', ''),
localeProvider
),
_buildLanguageItem(
context,
'일본어',
const Locale('ja', ''),
localeProvider
),
],
),
);
}
Widget _buildLanguageItem(
BuildContext context,
String languageName,
Locale locale,
LocaleProvider provider
) {
return ListTile(
title: Text(languageName),
trailing: provider.locale.languageCode == locale.languageCode
? const Icon(Icons.check)
: null,
onTap: () {
provider.setLocale(locale);
},
);
}
}
5. 로케일 영구 저장
사용자의 언어 선택을 저장하기 위해 shared_preferences
를 사용할 수 있습니다:
// providers/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('en', '');
static const String _localeKey = 'locale_language_code';
LocaleProvider() {
_loadSavedLocale();
}
Locale get locale => _locale;
Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final savedLanguageCode = prefs.getString(_localeKey);
if (savedLanguageCode != null) {
_locale = Locale(savedLanguageCode, '');
notifyListeners();
}
}
Future<void> setLocale(Locale locale) async {
_locale = locale;
// 선택된 언어 코드 저장
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_localeKey, locale.languageCode);
notifyListeners();
}
}
6. 날짜, 숫자, 통화 형식화
intl
패키지를 활용하여 로케일에 맞는 날짜, 숫자, 통화 형식을 지원할 수 있습니다:
import 'package:intl/intl.dart';
// 날짜 형식화
String formatDate(DateTime date, String locale) {
return DateFormat.yMMMd(locale).format(date);
}
// 통화 형식화
String formatCurrency(double amount, String locale, String currencyCode) {
return NumberFormat.currency(
locale: locale,
symbol: currencyCode,
).format(amount);
}
// 사용 예시
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context).toString();
final now = DateTime.now();
final price = 1234.56;
return Column(
children: [
Text('날짜: ${formatDate(now, locale)}'),
Text('가격: ${formatCurrency(price, locale, '₩')}'),
],
);
}
7. 이미지 및 에셋 현지화
언어에 따라 다른 이미지나 에셋을 사용해야 하는 경우:
// 언어별 이미지 경로 가져오기
String getLocalizedImagePath(BuildContext context) {
final locale = Localizations.localeOf(context).languageCode;
return 'assets/images/${locale}/banner.png';
}
// 사용 예시
Image.asset(getLocalizedImagePath(context))
8. 방향성 처리 (RTL 지원)
아랍어나 히브리어와 같은 오른쪽에서 왼쪽(RTL)으로 읽는 언어를 지원하려면:
Widget build(BuildContext context) {
// 현재 텍스트 방향 확인
final textDirection = Directionality.of(context);
return Row(
children: [
// textDirection이 RTL이면 아이콘과 텍스트의 순서가 반전됨
if (textDirection == TextDirection.ltr) Icon(Icons.arrow_forward),
Text(context.l10n.continue),
if (textDirection == TextDirection.rtl) Icon(Icons.arrow_back),
],
);
}
Flutter는 RTL 언어에 대한 자동 미러링을 지원합니다. MaterialApp
에서 이를 활성화할 수 있습니다:
MaterialApp(
supportedLocales: // ...,
localizationsDelegates: // ...,
builder: (context, child) {
return Directionality(
// 시스템 TextDirection 사용
textDirection: Directionality.of(context),
child: child!,
);
},
)
9. 고급 국제화 전략
9.1 번역 누락 감지
개발 및 테스트 단계에서 번역 누락을 감지하기 위한 헬퍼 메서드:
// helpers/missing_translation_detector.dart
import 'package:flutter/foundation.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MissingTranslationDetector {
static void checkMissingTranslations(BuildContext context) {
if (!kDebugMode) return; // 디버그 모드에서만 실행
final translations = AppLocalizations.of(context)!;
final locale = Localizations.localeOf(context);
// 영어 로케일의 모든 getter 메서드 추출
final englishMethods = AppLocalizations(const Locale('en', ''))
.runtimeType
.toString()
.split('(')
.first;
// TODO: 리플렉션을 통해 현재 로케일과 기준 로케일(영어) 간 누락된 번역 감지
print('현재 로케일: $locale');
print('기준 로케일 메서드: $englishMethods');
}
}
9.2 동적 로케일 로딩
모든 번역을 앱에 포함하지 않고 필요에 따라 서버에서 동적으로 로드하려면:
// services/remote_translations_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
class RemoteTranslationsService {
static Future<Map<String, String>> fetchTranslations(String languageCode) async {
try {
final response = await http.get(
Uri.parse('https://your-api.com/translations/$languageCode'),
);
if (response.statusCode == 200) {
return Map<String, String>.from(json.decode(response.body));
} else {
throw Exception('Failed to load translations');
}
} catch (e) {
print('Error fetching translations: $e');
return {};
}
}
}
// 커스텀 로컬라이제이션 대리자
class CustomLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
@override
bool isSupported(Locale locale) => ['en', 'ko', 'ja'].contains(locale.languageCode);
@override
Future<AppLocalizations> load(Locale locale) async {
// 로컬 번역 로드
AppLocalizations localizations = await AppLocalizations.delegate.load(locale);
// 서버에서 추가 번역 로드
if (useRemoteTranslations) {
Map<String, String> remoteTranslations =
await RemoteTranslationsService.fetchTranslations(locale.languageCode);
// 로컬 번역에 원격 번역 병합 (여기서는 간단한 예시)
// 실제로는 커스텀 AppLocalizations 확장 클래스 필요
}
return localizations;
}
@override
bool shouldReload(CustomLocalizationsDelegate old) => false;
}
10. 국제화 모범 사례
10.1 번역 키 체계화
// 계층적 구조의 번역 키 사용 예시
{
"auth_login_title": "로그인",
"auth_login_username_label": "사용자 이름",
"auth_login_password_label": "비밀번호",
"auth_login_button": "로그인하기",
"settings_general_title": "일반 설정",
"settings_account_title": "계정 설정"
}
10.2 번역 워크플로우 자동화
대규모 앱의 번역 관리를 위한 자동화:
- 번역 추출: ARB 템플릿에서 번역이 필요한 문자열 추출
- 번역 서비스 통합: Google Translate API 같은 서비스로 초기 번역 생성
- 번역 검수: 전문 번역가 검토
- 자동 병합: CI/CD 파이프라인에서 검수된 번역 자동 통합
10.3 컨텍스트 제공
번역자에게 충분한 컨텍스트를 제공하여 정확한 번역을 보장합니다:
{
"submit": "제출",
"@submit": {
"description": "Form submission button text",
"context": "Used in the contact form at the end of a long process"
}
}
결론
Flutter 앱의 국제화는 광범위한 사용자 기반에 접근하기 위한 필수적인 단계입니다. ARB 파일과 flutter_localizations
패키지를 활용하면 다양한 언어와 문화에 맞춘 앱을 쉽게 개발할 수 있습니다.
효과적인 국제화 전략에는 다음 요소가 포함됩니다:
- 기본 설정: 필요한 패키지와 로컬라이제이션 대리자 설정
- ARB 파일 관리: 체계적인 번역 키 구조 및 컨텍스트 제공
- UI 통합:
AppLocalizations.of(context)
또는 확장 메서드를 통한 번역 사용 - 로케일 관리: 사용자 언어 선택 및 설정 유지
- 형식화: 날짜, 숫자, 통화의 로케일별 형식화
- RTL 지원: 오른쪽에서 왼쪽으로 읽는 언어에 대한 레이아웃 조정
- 자동화: 번역 워크플로우 자동화로 대규모 앱의 번역 관리 효율화
앱의 초기 개발 단계부터 국제화를 고려하면 나중에 여러 언어 지원을 추가할 때 발생할 수 있는 많은 문제를 예방할 수 있습니다. 또한 언어별 테스트를 통해 모든 번역이 UI에 적절히 표시되는지 확인하는 것이 중요합니다.