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/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
옵션설명
arb-dirARB 파일이 위치한 디렉토리
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.delegateMaterial 위젯 번역
GlobalWidgetsLocalizations.delegate기본 위젯 방향(RTL/LTR)
GlobalCupertinoLocalizations.delegateCupertino 위젯 번역

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>시간
Hm()15<30>24시간제

Watch out - 주의사항#

intl 패키지 사용 전 initializeDateFormatting()을 호출해야 합니다:

import 'package:intl/date_symbol_data_local.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting(); // 모든 로케일 초기화
runApp(const MyApp());
}

실전 예제: 다국어 쇼핑 앱#

lib/l10n/app_en.arb
{
"@@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 다국어 지원의 핵심 개념을 정리하면:

  1. 패키지 설정: flutter_localizations, intl 의존성 추가
  2. ARB 파일 작성: 각 언어별 번역 파일 생성
  3. 코드 생성: flutter gen-l10n 또는 자동 생성
  4. MaterialApp 구성: 델리게이트와 지원 로케일 설정
  5. 번역 사용: AppLocalizations.of(context) 활용
  6. 형식화: intl 패키지로 날짜/숫자/통화 지역화

Footnotes#

  1. 국제화(i18n): Internationalization의 약자로, i와 n 사이에 18글자가 있어 i18n으로 표기합니다. 앱을 여러 언어로 확장할 수 있도록 준비하는 과정입니다.

  2. 지역화(l10n): Localization의 약자로, l과 n 사이에 10글자가 있어 l10n으로 표기합니다. 특정 언어/지역에 맞게 콘텐츠를 적용하는 과정입니다.

  3. ARB (Application Resource Bundle): Google에서 정의한 지역화 파일 형식으로, ICU 메시지 문법을 지원하는 JSON 기반 형식입니다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 39편: 다국어 지원(i18n)
https://moodturnpost.net/posts/flutter/flutter-internationalization/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차