다국어처리 (l10n, i18n)

다국어 처리를 왜 해야하지?

너무 뻔한 말인데 서버와 구분하고자 굳이 목차의 일부로 정의했다. 왜냐면 모바일에서는 다국어 처리가 아주 중요하기 때문이다. 서비스가 하나의 나라에서만 서비스할 작정이고 영원히 그렇게 될 것이면 상관이 없을 수 있으나 하나의 나라에서도 본인이 더 편한 언어가 다른 언어인 사용자가 있을 수 있다.

뿐만 아니라 다른 나라에 서비스 할 수 있다면 시장의 크기는 훨씬 커지는데 앱에서 다국어 처리가 대응되어 있지 않다면 서비스 자체가 어려워 진다. 서비스가 다른 나라에 런칭할 수 있는 성격이고, 그럴 계획마저 있다면 무조건 처리를 해두는 것이 맞다.

서버도 다국어 처리를 해야할까? 무엇을 기준으로 판단할까

핵심 기준은 '서버에서 처리한 텍스트가 그대로 사용자에게 전달되는가' 가 기준이라고 생각한다. 어찌보면 당연한 이야기인데 에러 메세지의 처리를 놓고 보았을때 서버에서 내려준 코드에 따라 클라이언트에서 한 번 가공을 하는 경우가 있는데 이런 경우 클라이언트가 재처리를 한 번 하기 때문에 서버가 굳이 다국어 처리를 해줄 필요가 없다.

하지만 서버에서 내려준 에러 메세지를 바로 노출시키는 것을 혼재시키는 경우가 있는데(코드에 따른 재처리와 함께) 이런 경우에는 서비스 국가가 다르고 서버가 하나의 서버만 존재한다면 서버에서도 다국어 처리를 고려해야 함이 옳다.

다국어 처리 구현

easy_localization 라이브러리 사용

easy_localization 을 사용한다. (제일 보편적인 라이브러리라서 택했다.)

flutter pub add easy_localization

runApp() 시 필요한 구현 사항(초기화 관련 구현)

  run() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Injector.registerDependencies();
    await EasyLocalization.ensureInitialized();
    runApp(
      EasyLocalization(
        startLocale: LocaleManager.koLocale,
        supportedLocales: const [
          LocaleManager.koLocale,
          LocaleManager.enLocale,
        ],
        path: 'assets/translations',
        fallbackLocale: LocaleManager.koLocale,
        child: const ProjectName(),
      ),
    );
  }
import 'dart:ui';

class LocaleManager {
  static const Locale enLocale = Locale('en');
  static const Locale koLocale = Locale('ko');
}

WidgetsFlutterBinding.ensureInitialized() 을 해줘야하는 이유

Flutter 앱은 Dart 레이어와 네이티브 레이어가 함께 동작하는데 이 두 레이어간의 상호 작용을 위해서는 Flutter 엔진에 Dart 코드를 바인딩을 명시적으로 처리해줘야 한다. 즉, 네이티브 자원을 끌어다 쓰는 단 하나의 로직이라도 앱이 가지고 있다면 이 처리를 꼭 해줘야 하고 easy_localization 에서 네이티브 자원을 사용하기 때문에 다국어 처리시 이 처리를 해줘야한다.

EasyLocalization.ensureInitialized() 을 해줘야하는 이유

environment 에서 위와 같이 처리해준다. EasyLocalization.ensureInitialized() 를 해줘야 하는 이유는 easy_localization 라이브러리가 앱이 구동되는 단계에 리소스를 로드 하는 등 초기화 작업이 필요하기 때문이다. 이를 명시적으로 호출해줘야 한다. 비동기 함수이므로 반드시 await 를 해줘서 끝나고 화면에 진입하도록 보장해야한다.

그 외 설정

startLocale 에서 시작 locale 지정을 해준다. fallbackLocale 은 path 의 경우는 startLocale 이 실패 했을때 지정될 locale 이다. path는 다국어 처리 내용을 key, value 형식으로 담은 폴더의 위치이다.

LocaleManager 의 경우 presentation layer 에 있다. 이것은 나중에 presentation layer 정리시 추가로 정리할 예정이다.

pubspec.yaml 에서 다국어 처리 관련 assets 지정

pubspec.yaml 에 위와 같이 translations 를 assets 으로 지정해준다.

실제 다국어처리 내용을 담은 json 파일 작성

ko.json
{
  "serviceName": "서비스명 한글"
}

en.json
{
  "serviceName": "service name en"
}

위와 같이 key, value 형식으로 동일 key 에 대해서 다른 국가의 언어로 다국어 처리를 한다.

TextManager 로 key 값 관리(오타 방지 등을 위한)

동일한 key 를 화면상에서 호출해야하므로 이를 관리하는 객체를 하나 두는 게 좋다고 생각된다. 나는 LocaleManager 와 동일한 성격으로 간주하여 presentation layer 에 TextManager 를 두었다.

class TextManager {
  static String serviceName = "serviceName";
}

앱 최초 진입시 처리

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';

import '../../barrel.dart';

class ProjectName extends StatefulWidget {
  const ProjectName({super.key});

  @override
  _ProjectNameState createState() => _ProjectNameState();
}

class _ProjectNameState extends State<ProjectName> with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.resumed) {
      // background to foreground 된 상태
    }
  }

  @override
  Widget build(BuildContext context) {
    return MediaQuery(
      data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
      child: MaterialApp.router(
        localizationsDelegates: context.localizationDelegates,
        supportedLocales: context.supportedLocales,
        locale: context.locale,
        debugShowCheckedModeBanner: false,
        routerConfig: RouterConfiguration.router,
      ),
    );
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}

지금까지 easy_localization 에 관한 설정을 해준 것이고 이제 이 설정이 실제 플러터 프레임워크에서 동작하도록 설정해주는 단계가 지금 단계이다. 지역화 관련 설정을 위임하는 구조이다.

사용

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_template/barrel.dart';

class SplashScreen extends StatelessWidget {
  const SplashScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(TextManager.serviceName.tr()),
      ),
    );
  }
}

key 값에서 .tr() 을 호출해서 실제 다국어 처리가 된 json 의 파일의 key 에 대응하는 value 값을 노출한다.

Last updated