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 번역 워크플로우 자동화

대규모 앱의 번역 관리를 위한 자동화:

  1. 번역 추출: ARB 템플릿에서 번역이 필요한 문자열 추출
  2. 번역 서비스 통합: Google Translate API 같은 서비스로 초기 번역 생성
  3. 번역 검수: 전문 번역가 검토
  4. 자동 병합: 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 패키지를 활용하면 다양한 언어와 문화에 맞춘 앱을 쉽게 개발할 수 있습니다.

효과적인 국제화 전략에는 다음 요소가 포함됩니다:

  1. 기본 설정: 필요한 패키지와 로컬라이제이션 대리자 설정
  2. ARB 파일 관리: 체계적인 번역 키 구조 및 컨텍스트 제공
  3. UI 통합: AppLocalizations.of(context) 또는 확장 메서드를 통한 번역 사용
  4. 로케일 관리: 사용자 언어 선택 및 설정 유지
  5. 형식화: 날짜, 숫자, 통화의 로케일별 형식화
  6. RTL 지원: 오른쪽에서 왼쪽으로 읽는 언어에 대한 레이아웃 조정
  7. 자동화: 번역 워크플로우 자동화로 대규모 앱의 번역 관리 효율화

앱의 초기 개발 단계부터 국제화를 고려하면 나중에 여러 언어 지원을 추가할 때 발생할 수 있는 많은 문제를 예방할 수 있습니다. 또한 언어별 테스트를 통해 모든 번역이 UI에 적절히 표시되는지 확인하는 것이 중요합니다.

results matching ""

    No results matching ""