Ollama나 LM Studio로 Flutter 앱 번역하기

업데이트 · LocalePilot

내 컴퓨터에서 실행되는 모델로 구독 없이, 텍스트를 온라인 서비스에 보내지 않고 ARB 파일의 번역 초안을 만들 수 있습니다. 설정 방법, 출발점이 될 Dart 스크립트, 그리고 모델이 자주 틀리는 부분을 소개합니다.

로컬 모델이 번역가를 대신하지는 못합니다. 그래도 짧은 UI 문자열이라면 무료로 쓸 만한 초안을 얻을 수 있고, 텍스트는 컴퓨터 밖으로 나가지 않습니다. 작업의 대부분은 모델 바깥에서 이루어집니다. 문맥을 주고, 플레이스홀더를 지키고, 돌아온 결과를 검토하는 일입니다.

모델 설치하기

Ollama에서는 명령 하나로 모델을 내려받고, 로컬 서버가 http://localhost:11434에서 대기합니다.

ollama pull gemma3:12b

120억 매개변수 모델은 기본 양자화 빌드로 16 GB 메모리에 들어갑니다. 작은 컴퓨터에서는 gemma3:4b로 간단한 문자열을 처리할 수 있지만, 긴 문장에서는 실수가 늘어납니다.

LM Studio에서는 앱에서 모델을 고르고 로컬 서버를 시작합니다. OpenAI 호환 API가 기본적으로 http://localhost:1234/v1에 열립니다.

모델에게 알려 줘야 할 것

"Share list"만 보내면 모델은 그것이 버튼인지, 제목인지, 동사인지 추측할 수밖에 없습니다. 확실히 차이를 만드는 것은 세 가지입니다.

  • 키의 설명: ARB 파일에 @key.description을 채워 두었다면 쓸 수 있습니다. 설명을 쓰는 가장 좋은 이유입니다.
  • 앱의 문맥: "가족용 장보기 목록 앱, 쉬운 어조, 프랑스어에서는 친근한 tu" 같은 정보.
  • 형식 규칙: 모든 {placeholder}와 ICU 구문을 그대로 두고, 번역만 답할 것.

temperature도 0으로 설정하세요. 번역에 필요한 것은 가장 가능성 높은 답입니다.

출발점이 될 스크립트

이 Dart 스크립트는 app_fr.arb에서 빠진(또는 비어 있는) 키를 Ollama로 하나씩 번역합니다. ARB 파일은 건드리지 않습니다. 초안은 별도 파일에 저장되고, 플레이스홀더가 바뀐 답은 따로 제외됩니다.

import 'dart:convert';
import 'dart:io';

const model = 'gemma3:12b';
final placeholder = RegExp(r'\{[A-Za-z_]\w*\}');

Future<void> main() async {
  final source = _read('lib/l10n/app_en.arb');
  final target = _read('lib/l10n/app_fr.arb');
  final drafts = <String, String>{};

  for (final key in source.keys.where((k) => !k.startsWith('@'))) {
    if ((target[key] as String?)?.isNotEmpty ?? false) continue;
    final text = source[key] as String;
    final description = (source['@$key'] as Map?)?['description'] ?? '';
    final draft = await _translate(text, description as String);
    if (_placeholders(draft) != _placeholders(text)) {
      stderr.writeln('$key: placeholders changed, redo by hand');
      continue;
    }
    drafts[key] = draft;
  }

  File('app_fr.drafts.json').writeAsStringSync(const JsonEncoder.withIndent('  ').convert(drafts));
  print('${drafts.length} drafts in app_fr.drafts.json');
}

Map<String, dynamic> _read(String path) => jsonDecode(File(path).readAsStringSync()) as Map<String, dynamic>;

String _placeholders(String text) => (placeholder.allMatches(text).map((m) => m[0]!).toList()..sort()).join(',');

Future<String> _translate(String text, String description) async {
  final client = HttpClient();
  final request = await client.postUrl(Uri.parse('http://localhost:11434/api/generate'));
  request.headers.contentType = ContentType.json;
  request.write(jsonEncode({
    'model': model,
    'stream': false,
    'options': {'temperature': 0},
    'prompt': 'Translate this mobile app string from English to French. '
        'Keep every {placeholder} and the ICU plural/select syntax unchanged. '
        'Context: $description\n'
        'Reply with the translation only.\n\n$text',
  }));
  final response = await request.close();
  final body = jsonDecode(await response.transform(utf8.decoder).join()) as Map<String, dynamic>;
  client.close();
  return (body['response'] as String).trim();
}

프롬프트에 앱의 문맥을 추가하고, 대상 언어가 프랑스어가 아니라면 바꾸세요(한국어라면 app_ko.arb와 "to Korean").

잘못되는 부분

ICU 복수형. 가장 약한 부분입니다. 모델은 other 키워드를 번역하거나, 분기를 빠뜨리거나, 한 분기에서만 {count}의 이름을 바꿀 수 있습니다. 스크립트의 플레이스홀더 비교로 일부는 잡을 수 있지만, 나머지는 모든 복수형을 검토해서 확인하세요. 프랑스어에서 0에도 적용되는 =1 분기도 주의해야 합니다(자세한 내용은 ICU 복수형 가이드 참고).

고유 명사. 모델은 앱 이름, 유료 플랜 이름, 기능 이름까지 번역해 버리기 쉽습니다. 프롬프트에 유지할 용어로 나열하고, 나중에 확인하세요.

일관성. 문자열은 하나씩 따로 번역되므로 "item"이 어떤 곳에서는 "article", 다른 곳에서는 "élément"이 될 수 있습니다. 프롬프트에 짧은 용어집을 넣으면 대부분 해결됩니다.

길이. 프랑스어와 독일어는 영어보다 길어지기 쉽습니다. 영어에서 들어맞던 버튼이 넘칠 수 있으며, 이는 화면에서만 확인할 수 있습니다.

LM Studio의 경우

바뀌는 것은 요청뿐입니다. API는 OpenAI 형식을 따릅니다. 메시지 목록을 /v1/chat/completions로 보내고 응답에서 choices[0].message.content를 읽습니다. 스크립트의 나머지는 그대로입니다.

검토한 뒤 병합하기

초안 파일을 app_en.arb 옆에 열고 키를 하나씩 살펴봅니다. 좋은 것은 app_fr.arb에 복사하고, flutter gen-l10n을 다시 실행한 뒤 해당 화면을 확인합니다. LocalePilot은 이 과정을 자동화합니다. Ollama나 LM Studio의 제안은 "검토할 AI 초안"으로 작업 공간에 들어오고, QA가 플레이스홀더를 비교하며, 내보내기 전까지 파일에는 아무것도 기록되지 않습니다.

자주 묻는 질문

앱 문자열에는 어떤 모델을 써야 하나요?

gemma3:12b 같은 최신 범용 12B 모델이면 짧은 문자열에 좋은 초안을 얻을 수 있습니다. 작은 컴퓨터에서는 4B 버전도 동작하지만 실수가 늘어납니다.

텍스트가 내 컴퓨터 밖으로 나가나요?

아닙니다. Ollama와 LM Studio는 모델을 로컬에서 실행하며, 요청은 localhost로 갑니다.

ARB 파일에 바로 써도 되나요?

초안 파일에 쓰고 검토한 뒤 병합하는 편이 낫습니다. 모델이 ICU 복수형을 깨뜨리거나 브랜드명을 번역할 수 있기 때문입니다.