245 lines
8.6 KiB
Dart
245 lines
8.6 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:fancake/core/i18n/app_locale.dart';
|
|
import 'package:fancake/features/my/my_data.dart';
|
|
import 'package:fancake/features/my/settings_screen.dart';
|
|
import 'package:fancake/l10n/app_localizations.dart'
|
|
show lookupAppLocalizations;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../tool/gen_l10n_remote.dart';
|
|
|
|
Map<String, dynamic> _arb(String lang) =>
|
|
jsonDecode(File('lib/l10n/app_$lang.arb').readAsStringSync())
|
|
as Map<String, dynamic>;
|
|
|
|
Set<String> _keys(Map<String, dynamic> arb) =>
|
|
arb.keys.where((k) => !k.startsWith('@')).toSet();
|
|
|
|
Widget _app(LocaleController c, Widget home) => ListenableBuilder(
|
|
listenable: c,
|
|
builder: (_, _) => LocaleScope(
|
|
controller: c,
|
|
child: MaterialApp(
|
|
locale: c.locale,
|
|
supportedLocales: appLocales,
|
|
localizationsDelegates: [
|
|
RemoteL10nDelegate(c.remote),
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
home: home,
|
|
),
|
|
),
|
|
);
|
|
|
|
void main() {
|
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
|
|
|
group('지원 언어 (원장 B-76)', () {
|
|
test('supportedLocales 가 정확히 ko · en · ja · zh-CN · es 다', () {
|
|
expect(appLocales.map(localeTag).toList(), [
|
|
'ko',
|
|
'en',
|
|
'ja',
|
|
'zh-CN',
|
|
'es',
|
|
]);
|
|
});
|
|
|
|
test('단말 언어로 기본값을 고르고 zh-TW·zh-Hant 는 zh-CN · 미지원 언어는 en 으로 폴백한다', () {
|
|
expect(resolveAppLocale(const [Locale('ja', 'JP')]), const Locale('ja'));
|
|
expect(
|
|
resolveAppLocale(const [Locale('zh', 'TW')]),
|
|
const Locale('zh', 'CN'),
|
|
);
|
|
expect(
|
|
resolveAppLocale(const [
|
|
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),
|
|
]),
|
|
const Locale('zh', 'CN'),
|
|
);
|
|
expect(resolveAppLocale(const [Locale('fr', 'FR')]), const Locale('en'));
|
|
expect(
|
|
resolveAppLocale(const [Locale('fr'), Locale('es', 'MX')]),
|
|
const Locale('es'),
|
|
);
|
|
});
|
|
|
|
test('5개 언어 arb 가 같은 키를 전부 가진다(QA 8-2 문자열 누락 0)', () {
|
|
final ko = _keys(_arb('ko'));
|
|
for (final lang in const ['en', 'ja', 'zh', 'es']) {
|
|
final other = _keys(_arb(lang));
|
|
expect(ko.difference(other), isEmpty, reason: '$lang 누락');
|
|
expect(other.difference(ko), isEmpty, reason: '$lang 초과');
|
|
for (final k in other) {
|
|
expect(
|
|
(_arb(lang)[k] as String).trim(),
|
|
isNotEmpty,
|
|
reason: '$lang.$k',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('원격 덮어쓰기 층이 arb 의 모든 키를 덮는다(생성물이 최신)', () {
|
|
final generated = File(
|
|
'lib/l10n/remote_app_localizations.dart',
|
|
).readAsStringSync();
|
|
final expected = generateRemoteL10n(_arb('ko'));
|
|
for (final key in _keys(_arb('ko'))) {
|
|
expect(expected, contains(' $key'));
|
|
expect(generated, contains(' $key'), reason: '재생성 필요: $key');
|
|
}
|
|
});
|
|
});
|
|
|
|
group('언어 전환 · 원격 문자열 (FR-I18N-01)', () {
|
|
testWidgets('설정의 언어 시트에서 English 를 고르면 화면 문자열이 바뀌고 기기에 남는다', (
|
|
tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 2;
|
|
addTearDown(tester.view.reset);
|
|
String? saved;
|
|
final c = LocaleController(
|
|
initial: fallbackLocale,
|
|
onSaved: (tag) async => saved = tag,
|
|
);
|
|
await tester.pumpWidget(
|
|
_app(c, SettingsScreen(repository: SampleMyRepository())),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('설정'), findsOneWidget);
|
|
|
|
await tester.scrollUntilVisible(find.text('한국어'), 200);
|
|
await tester.tap(find.text('한국어'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('English'));
|
|
await tester.tap(find.text('선택'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(c.locale, const Locale('en'));
|
|
expect(find.text('Settings'), findsOneWidget);
|
|
expect(saved, 'en');
|
|
final prefs = await SharedPreferences.getInstance();
|
|
expect(prefs.getString(LocaleController.prefsKey), 'en');
|
|
});
|
|
|
|
testWidgets('원격 문서의 키는 번들 문자열을 덮고, 없는 키는 번들을 쓴다', (tester) async {
|
|
final remote = StreamController<Map<String, Object?>>();
|
|
addTearDown(remote.close);
|
|
final c = LocaleController(
|
|
initial: const Locale('ja'),
|
|
remoteSource: (tag) {
|
|
expect(tag, 'ja');
|
|
return remote.stream;
|
|
},
|
|
);
|
|
await tester.pumpWidget(
|
|
_app(
|
|
c,
|
|
Builder(
|
|
builder: (context) => Column(
|
|
children: [
|
|
Text(context.l10n.settingsTitle),
|
|
Text(context.l10n.commonNotYet('X')),
|
|
Text(context.l10n.commonCancel),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(find.text('設定'), findsOneWidget);
|
|
|
|
remote.add({
|
|
'settingsTitle': '設定(更新)',
|
|
'commonNotYet': '{where} まもなく',
|
|
'ignored': 3,
|
|
});
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('設定(更新)'), findsOneWidget);
|
|
expect(find.text('X まもなく'), findsOneWidget);
|
|
expect(find.text('キャンセル'), findsOneWidget);
|
|
});
|
|
|
|
test('원격 문자열은 인자 값 안의 자리표시자를 다시 치환하지 않는다', () {
|
|
final t = RemoteL10nDelegate(const {
|
|
'linkTitle': '{name} 연동',
|
|
'myShareText': '{nickname}: {level}',
|
|
});
|
|
return t.load(const Locale('ko')).then((l10n) {
|
|
expect(l10n.linkTitle('{name}'), '{name} 연동');
|
|
expect(l10n.myShareText('{level}', 'A', '1'), '{level}: A');
|
|
});
|
|
});
|
|
|
|
test('언어를 복원하면 이전 언어의 원격 문자열을 버린다', () async {
|
|
final remote = StreamController<Map<String, Object?>>.broadcast();
|
|
addTearDown(remote.close);
|
|
final c = LocaleController(
|
|
initial: const Locale('ja'),
|
|
remoteSource: (_) => remote.stream,
|
|
);
|
|
remote.add({'settingsTitle': '設定(更新)'});
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(c.remote, isNotEmpty);
|
|
SharedPreferences.setMockInitialValues({LocaleController.prefsKey: 'es'});
|
|
await c.restore();
|
|
expect(c.locale, const Locale('es'));
|
|
expect(c.remote, isEmpty);
|
|
});
|
|
|
|
test('로그인 때 이 기기에서 고른 언어가 없으면 프로필 언어를 따른다', () async {
|
|
final c = LocaleController(initial: const Locale('en'));
|
|
await c.adoptProfile('ja');
|
|
expect(c.locale, const Locale('ja'));
|
|
});
|
|
|
|
test('이 기기에서 고른 언어가 있으면 프로필 언어보다 우선한다', () async {
|
|
SharedPreferences.setMockInitialValues({LocaleController.prefsKey: 'es'});
|
|
final c = LocaleController(initial: const Locale('es'));
|
|
await c.adoptProfile('ja');
|
|
expect(c.locale, const Locale('es'));
|
|
});
|
|
|
|
test('저장된 언어를 복원한다', () async {
|
|
SharedPreferences.setMockInitialValues({
|
|
LocaleController.prefsKey: 'zh-CN',
|
|
});
|
|
final c = LocaleController(initial: fallbackLocale);
|
|
await c.restore();
|
|
expect(c.locale, const Locale('zh', 'CN'));
|
|
});
|
|
|
|
test('대리자가 없는 트리는 ko 로 폴백한다', () {
|
|
expect(koL10n.settingsTitle, '설정');
|
|
expect(
|
|
lookupAppLocalizations(const Locale('zh', 'CN')).settingsTitle,
|
|
'设置',
|
|
);
|
|
});
|
|
});
|
|
|
|
// 10/4 실기기 — 「발주사」 · 「09-10 회차에 연결」 · 오탈자 「맴버」가 사용자 화면에 떴다.
|
|
test('사용자 문구에 내부 표현 · 일정 · 오탈자가 없다', () {
|
|
const banned = ['발주사', '発注元', 'client key', 'MOCK', '회차에 연결', '09-10', '2026-', 'Q-QC-', '맴버', '구현 확인물'];
|
|
for (final file in Directory('lib/l10n').listSync().whereType<File>()) {
|
|
if (!file.path.endsWith('.arb')) continue;
|
|
final arb = jsonDecode(file.readAsStringSync()) as Map<String, dynamic>;
|
|
for (final MapEntry(:key, :value) in arb.entries) {
|
|
if (key.startsWith('@') || value is! String) continue;
|
|
for (final word in banned) {
|
|
expect(value.contains(word), isFalse, reason: '${file.path} $key: $value');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|