Flutter 기본 글을 화면 언어로 (140 · 158): 뒤로 · 라이선스 화면 · 복사 · 붙여넣기 메뉴 등
- flutter_localizations 를 더하고 MaterialApp 이 화면 언어 (i18n) 를 바꿀 때만 다시 그려진다. - Flutter 가 아는 언어면 그 언어 (앱이 더한 기계 번역 언어 포함, 예 de · vi), 모르는 언어일 때만 영어. 한국어가 섞이지 않는다. - supportedLocales 는 kMaterialSupportedLanguages 전체 + 지금 언어 (zh-Hant · pt-BR 처럼 글자 체계 · 나라까지 그대로). - Material 만 알고 Cupertino 는 모르는 언어 (ps) 는 Cupertino 글만 영어로 채운다. - 시험: locale_test (de → Zurück, 없는 언어 → Back, zh-Hant 유지, ps 열림). - Windows 캡처 시험 (windows_i18n_shot_test): 라이선스 목록을 다 읽은 뒤까지 찍는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음4
parent
f331672e3a
commit
4c0bc0b9ce
|
|
@ -0,0 +1,80 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart' show kSecondaryButton;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/i18n_controller.dart';
|
||||
import 'package:jj_mkvmaker/main.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/license_texts.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 140 · 158 기기 감독 확인용 (Windows): 실제 앱 (JjMkvMakerApp) 에서 Flutter 기본 화면 · 버튼이 화면 언어로 나오는지 PNG (JJ_SHOT_DIR).
|
||||
/// 라이선스 화면 · 글 칸의 복사 · 붙여넣기 메뉴 · 뒤로 버튼 이름. 설정은 메모리에만 (저장하지 않음).
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_i18n');
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
final shotKey = GlobalKey();
|
||||
registerAppLicenses();
|
||||
|
||||
Future<void> settle(WidgetTester t, [int rounds = 10]) async {
|
||||
for (var i = 0; i < rounds; i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
|
||||
await t.pump();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> shot(WidgetTester t, String name) async {
|
||||
await settle(t, 3);
|
||||
// 앱 전체 (창 · 메뉴 · 말풍선까지 - 앱을 감싼 경계)
|
||||
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
final img = await t.runAsync(() => b.toImage());
|
||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||
Directory(out).createSync(recursive: true);
|
||||
File(p.join(out, '$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
for (final lang in ['ko', 'ja']) {
|
||||
testWidgets('140 · 158: Flutter 기본 화면 · 버튼이 화면 언어로 ($lang)', (t) async {
|
||||
t.view.physicalSize = const Size(1280, 800);
|
||||
t.view.devicePixelRatio = 1;
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
await t.runAsync(() => t.pumpWidget(RepaintBoundary(key: shotKey, child: JjMkvMakerApp(controller: c, navigatorKey: navKey))));
|
||||
await settle(t);
|
||||
// 1) 라이선스 화면 (제목 · 뒤로)
|
||||
showLicensePage(context: navKey.currentContext!, applicationName: 'JJ_MKVMaker');
|
||||
await settle(t, 15);
|
||||
await shot(t, 'i18n_${lang}_licenses');
|
||||
// 목록을 다 읽은 뒤 (줄마다 "라이선스 n개" 같은 개수 글이 화면 언어로)
|
||||
for (var i = 0; i < 600 && find.text('aria2').evaluate().isEmpty; i++) {
|
||||
await settle(t, 1);
|
||||
}
|
||||
await shot(t, 'i18n_${lang}_licenses_loaded');
|
||||
// 2) 뒤로 버튼 이름 (말풍선)
|
||||
await t.longPress(find.byType(BackButton).first);
|
||||
await settle(t, 5);
|
||||
await shot(t, 'i18n_${lang}_back_tooltip');
|
||||
navKey.currentState!.pop();
|
||||
await settle(t, 5);
|
||||
// 3) 글 칸의 복사 · 붙여넣기 메뉴 (오른쪽 클릭)
|
||||
final ctl = TextEditingController(text: 'JJ_MKVMaker 123');
|
||||
showDialog<void>(
|
||||
context: navKey.currentContext!,
|
||||
builder: (_) => AlertDialog(content: SizedBox(width: 360, child: TextField(controller: ctl))),
|
||||
);
|
||||
await settle(t, 5);
|
||||
await t.tapAt(t.getCenter(find.byType(TextField)), buttons: kSecondaryButton);
|
||||
await settle(t, 5);
|
||||
await shot(t, 'i18n_${lang}_text_menu');
|
||||
await t.runAsync(() => i18n.apply('ko', save: false));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart' show CupertinoLocalizations, DefaultCupertinoLocalizations;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
|
@ -686,8 +688,47 @@ class JjMkvMakerApp extends StatelessWidget {
|
|||
this.onExit,
|
||||
});
|
||||
|
||||
/// 140 · 158: Flutter 기본 글 (뒤로 · Licenses 등) 을 화면 언어로 - 앱 언어 코드 → Locale
|
||||
/// 앱이 더한 언어 (기계 번역 사전) 도 Flutter 가 아는 언어면 그 언어로, 모르는 언어일 때만 영어로 (한국어가 섞이지 않게).
|
||||
/// "zh-Hant" · "pt-BR" 같은 코드는 글자 체계 (4글자) · 나라 (2글자 · 숫자) 로 나눈다.
|
||||
static Locale localeOf(String code) {
|
||||
final parts = code.split(RegExp('[-_]'));
|
||||
String? script;
|
||||
String? country;
|
||||
for (final s in parts.skip(1)) {
|
||||
if (s.length == 4) {
|
||||
script = s;
|
||||
} else if (s.isNotEmpty) {
|
||||
country = s;
|
||||
}
|
||||
}
|
||||
final l = Locale.fromSubtags(languageCode: parts.first.toLowerCase(), scriptCode: script, countryCode: country?.toUpperCase());
|
||||
return GlobalMaterialLocalizations.delegate.isSupported(l) ? l : const Locale('en');
|
||||
}
|
||||
|
||||
/// Flutter 기본 글 사전들. Material 은 알고 Cupertino 는 모르는 언어 (예: ps) 에서도 Cupertino 글이 비지 않게 영어로 채운다.
|
||||
static const localizationsDelegates = [...GlobalMaterialLocalizations.delegates, _CupertinoFallbackDelegate()];
|
||||
|
||||
/// Flutter 가 아는 모든 언어. 지금 언어를 맨 앞에 두어 글자 체계 · 나라까지 그대로 고르게 ("zh-Hant" 가 간체로 바뀌지 않게).
|
||||
static List<Locale> supportedLocalesFor(Locale current) => [
|
||||
current,
|
||||
for (final c in kMaterialSupportedLanguages)
|
||||
if (c != current.languageCode || current.scriptCode != null || current.countryCode != null) Locale(c),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
Widget build(BuildContext context) => ListenableBuilder(
|
||||
// 화면 언어를 바꾸면 (i18n) Flutter 기본 글도 그 언어로
|
||||
listenable: i18n,
|
||||
builder: (context, _) => _app(context),
|
||||
);
|
||||
|
||||
Widget _app(BuildContext context) {
|
||||
final locale = localeOf(uiLanguage);
|
||||
return MaterialApp(
|
||||
locale: locale,
|
||||
supportedLocales: supportedLocalesFor(locale),
|
||||
localizationsDelegates: localizationsDelegates,
|
||||
title: appTitle,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildTheme(),
|
||||
|
|
@ -703,6 +744,21 @@ class JjMkvMakerApp extends StatelessWidget {
|
|||
child: AppDropArea(c: controller, child: _SystemBarsArea(child: UiScaler(c: controller, child: child!)))),
|
||||
home: HomePage(c: controller, downloads: downloads, onExit: onExit, bookmarks: bookmarks),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cupertino 글이 없는 언어만 영어로 (GlobalCupertinoLocalizations 뒤에 두어 그쪽이 아는 언어는 그쪽이 먼저)
|
||||
class _CupertinoFallbackDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
|
||||
const _CupertinoFallbackDelegate();
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => true;
|
||||
|
||||
@override
|
||||
Future<CupertinoLocalizations> load(Locale locale) => DefaultCupertinoLocalizations.load(locale);
|
||||
|
||||
@override
|
||||
bool shouldReload(_CupertinoFallbackDelegate old) => false;
|
||||
}
|
||||
|
||||
/// Android: 앱 화면이 상태 표시줄 · 아래쪽 작업 표시줄 밑까지 그려지므로 (Android 15 부터 기본)
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ packages:
|
|||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
# 140 · 158: Flutter 기본 화면 · 버튼 (뒤로 · 라이선스 · 날짜 고르기 등) 을 화면 언어로
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:flutter/cupertino.dart' show CupertinoLocalizations;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/main.dart';
|
||||
|
||||
void main() {
|
||||
test('140 · 158: 앱 화면 언어 → Flutter 기본 글의 언어', () {
|
||||
expect(JjMkvMakerApp.localeOf('ko'), const Locale('ko'));
|
||||
expect(JjMkvMakerApp.localeOf('en'), const Locale('en'));
|
||||
expect(JjMkvMakerApp.localeOf('ja'), const Locale('ja'));
|
||||
expect(JjMkvMakerApp.localeOf('zh-Hans'), const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'));
|
||||
expect(JjMkvMakerApp.localeOf('zh-Hant'), const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'));
|
||||
expect(JjMkvMakerApp.localeOf('pt-BR'), const Locale('pt', 'BR'));
|
||||
// 앱이 더한 언어 (기계 번역 사전) 도 Flutter 가 알면 그 언어로 - 한국어가 섞이지 않게
|
||||
expect(JjMkvMakerApp.localeOf('de'), const Locale('de'));
|
||||
expect(JjMkvMakerApp.localeOf('vi'), const Locale('vi'));
|
||||
// Flutter 가 모르는 언어만 영어
|
||||
expect(JjMkvMakerApp.localeOf('xx'), const Locale('en'));
|
||||
expect(JjMkvMakerApp.localeOf('tlh'), const Locale('en'));
|
||||
});
|
||||
|
||||
Future<String> backTooltip(WidgetTester tester, String code) async {
|
||||
final locale = JjMkvMakerApp.localeOf(code);
|
||||
late String text;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
key: ValueKey(code),
|
||||
locale: locale,
|
||||
supportedLocales: JjMkvMakerApp.supportedLocalesFor(locale),
|
||||
localizationsDelegates: JjMkvMakerApp.localizationsDelegates,
|
||||
home: Builder(builder: (context) {
|
||||
text = MaterialLocalizations.of(context).backButtonTooltip;
|
||||
return const SizedBox();
|
||||
}),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
return text;
|
||||
}
|
||||
|
||||
testWidgets('140 · 158: 뒤로 버튼 이름이 화면 언어로 (모르는 언어만 영어)', (tester) async {
|
||||
expect(await backTooltip(tester, 'ko'), '뒤로');
|
||||
expect(await backTooltip(tester, 'de'), 'Zurück');
|
||||
expect(await backTooltip(tester, 'xx'), 'Back');
|
||||
});
|
||||
|
||||
testWidgets('140 · 158: Material 만 아는 언어 (Cupertino 글 없음) 도 열림 - Cupertino 글은 영어', (tester) async {
|
||||
final only = kMaterialSupportedLanguages.where((c) => !GlobalCupertinoLocalizations.delegate.isSupported(Locale(c))).toList();
|
||||
for (final c in only.isEmpty ? ['ko'] : only) {
|
||||
final locale = JjMkvMakerApp.localeOf(c);
|
||||
expect(locale, Locale(c));
|
||||
late String cup;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
key: ValueKey(c),
|
||||
locale: locale,
|
||||
supportedLocales: JjMkvMakerApp.supportedLocalesFor(locale),
|
||||
localizationsDelegates: JjMkvMakerApp.localizationsDelegates,
|
||||
home: Builder(builder: (context) {
|
||||
cup = CupertinoLocalizations.of(context).cutButtonLabel;
|
||||
return const SizedBox();
|
||||
}),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull, reason: c);
|
||||
expect(cup, isNotEmpty, reason: c);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('140 · 158: 번체 중국어는 간체로 바뀌지 않음 (글자 체계까지 고름)', (tester) async {
|
||||
late Locale got;
|
||||
final locale = JjMkvMakerApp.localeOf('zh-Hant');
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
locale: locale,
|
||||
supportedLocales: JjMkvMakerApp.supportedLocalesFor(locale),
|
||||
localizationsDelegates: JjMkvMakerApp.localizationsDelegates,
|
||||
home: Builder(builder: (context) {
|
||||
got = Localizations.localeOf(context);
|
||||
return const SizedBox();
|
||||
}),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
expect(got.scriptCode, 'Hant');
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue