1
0
forked from baron/baron-sso

feat: i18n 개선 및 userfront 로그인/로케일 보완

This commit is contained in:
Lectom C Han
2026-02-12 21:25:26 +09:00
parent b6d3b69cda
commit dfa2fc2406
60 changed files with 5724 additions and 1734 deletions

View File

@@ -0,0 +1,5 @@
import 'web_storage_stub.dart'
if (dart.library.html) 'web_storage_web.dart';
export 'web_storage_stub.dart'
if (dart.library.html) 'web_storage_web.dart';

View File

@@ -0,0 +1,21 @@
class WebStorage {
bool get isWeb => false;
String? get(String key) => null;
void set(String key, String value) {}
String? getSession(String key) => null;
void setSession(String key, String value) {}
void removeSession(String key) {}
void clearSession() {}
void remove(String key) {}
void clear() {}
}
final webStorage = WebStorage();

View File

@@ -0,0 +1,37 @@
// ignore_for_file: avoid_web_libraries_in_flutter
import 'dart:html' as html;
class WebStorage {
bool get isWeb => true;
String? get(String key) => html.window.localStorage[key];
void set(String key, String value) {
html.window.localStorage[key] = value;
}
String? getSession(String key) => html.window.sessionStorage[key];
void setSession(String key, String value) {
html.window.sessionStorage[key] = value;
}
void removeSession(String key) {
html.window.sessionStorage.remove(key);
}
void clearSession() {
html.window.sessionStorage.clear();
}
void remove(String key) {
html.window.localStorage.remove(key);
}
void clear() {
html.window.localStorage.clear();
}
}
final webStorage = WebStorage();

View File

@@ -0,0 +1,88 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:userfront/core/i18n/locale_storage.dart';
import 'package:userfront/core/i18n/locale_storage_web.dart' as locale_web;
import 'helpers/web_storage.dart';
void main() {
setUp(() {
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(false);
locale_web.LocaleStorageImpl.forceSessionStorageForTests(false);
if (webStorage.isWeb) {
webStorage.clear();
webStorage.clearSession();
}
});
tearDown(() {
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(false);
locale_web.LocaleStorageImpl.forceSessionStorageForTests(false);
if (webStorage.isWeb) {
webStorage.clear();
webStorage.clearSession();
}
});
test(
'localStorage write/read (웹)',
() {
if (!webStorage.isWeb) {
return;
}
LocaleStorage.write('ko');
expect(webStorage.get('locale'), 'ko');
expect(LocaleStorage.read(), 'ko');
},
skip: !webStorage.isWeb,
);
test(
'legacy key에서 locale로 마이그레이션 (웹)',
() {
if (!webStorage.isWeb) {
return;
}
webStorage.set('baron_locale', 'en');
expect(LocaleStorage.read(), 'en');
expect(webStorage.get('locale'), 'en');
expect(webStorage.get('baron_locale'), isNull);
},
skip: !webStorage.isWeb,
);
test(
'localStorage 접근이 차단되면 메모리 fallback (웹)',
() {
if (!webStorage.isWeb) {
return;
}
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(true);
LocaleStorage.write('en');
expect(webStorage.get('locale'), isNull);
expect(webStorage.getSession('locale'), isNull);
expect(LocaleStorage.read(), 'en');
},
skip: !webStorage.isWeb,
);
test(
'localStorage 접근이 차단되면 sessionStorage로 fallback (웹)',
() {
if (!webStorage.isWeb) {
return;
}
locale_web.LocaleStorageImpl.forceSessionStorageForTests(true);
LocaleStorage.write('ko');
expect(webStorage.get('locale'), isNull);
expect(webStorage.getSession('locale'), 'ko');
expect(LocaleStorage.read(), 'ko');
},
skip: !webStorage.isWeb,
);
}

View File

@@ -0,0 +1,66 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:userfront/core/i18n/locale_utils.dart';
void main() {
group('locale_utils', () {
test('normalizeLocaleCode handles supported locales', () {
expect(normalizeLocaleCode('ko'), 'ko');
expect(normalizeLocaleCode('ko-KR'), 'ko');
expect(normalizeLocaleCode('en'), 'en');
expect(normalizeLocaleCode('en-US'), 'en');
});
test('normalizeLocaleCode falls back to default', () {
expect(normalizeLocaleCode('ja'), defaultLocaleCode);
expect(normalizeLocaleCode(null), defaultLocaleCode);
expect(normalizeLocaleCode(''), defaultLocaleCode);
});
test('extractLocaleFromPath picks locale when present', () {
expect(extractLocaleFromPath(Uri.parse('/ko/signin')), 'ko');
expect(extractLocaleFromPath(Uri.parse('/en/profile')), 'en');
expect(extractLocaleFromPath(Uri.parse('/ko')), 'ko');
});
test('extractLocaleFromPath returns null when missing', () {
expect(extractLocaleFromPath(Uri.parse('/signin')), isNull);
expect(extractLocaleFromPath(Uri.parse('/zz/signin')), isNull);
});
test('stripLocalePath removes locale segment', () {
expect(stripLocalePath(Uri.parse('/ko/signin')), '/signin');
expect(stripLocalePath(Uri.parse('/en/profile')), '/profile');
expect(stripLocalePath(Uri.parse('/ko')), '/');
expect(stripLocalePath(Uri.parse('/en/')), '/');
});
test('stripLocalePath keeps path without locale', () {
expect(stripLocalePath(Uri.parse('/signin')), '/signin');
expect(stripLocalePath(Uri.parse('/auth/callback')), '/auth/callback');
});
test('buildLocalizedPath applies locale', () {
expect(buildLocalizedPath('ko', Uri.parse('/signin')), '/ko/signin');
expect(buildLocalizedPath('en', Uri.parse('/signin')), '/en/signin');
expect(buildLocalizedPath('ko', Uri.parse('/')), '/ko');
expect(buildLocalizedPath('en', Uri.parse('/')), '/en');
});
test('buildLocalizedPath preserves query parameters', () {
final uri = Uri.parse('/signin?redirect_uri=https://example.com');
expect(
buildLocalizedPath('ko', uri),
'/ko/signin?redirect_uri=https%3A%2F%2Fexample.com',
);
});
test('buildLocalizedPath replaces existing locale', () {
expect(buildLocalizedPath('en', Uri.parse('/ko/signin')), '/en/signin');
expect(buildLocalizedPath('ko', Uri.parse('/en/profile')), '/ko/profile');
});
test('buildLocalizedPath drops unknown 2-letter prefix', () {
expect(buildLocalizedPath('ko', Uri.parse('/zz/signin')), '/ko/signin');
});
});
}

View File

@@ -5,15 +5,36 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:userfront/main.dart' show BaronSSOApp;
class _TestAssetLoader extends AssetLoader {
const _TestAssetLoader();
@override
Future<Map<String, dynamic>> load(String path, Locale locale) async {
return {};
}
}
void main() {
testWidgets('BaronSSOApp builds', (WidgetTester tester) async {
// runApp에서 ProviderScope로 감싸서 쓰고 있으니 테스트도 동일하게 감쌈
await tester.pumpWidget(const ProviderScope(child: BaronSSOApp()));
await tester.pumpWidget(
EasyLocalization(
supportedLocales: const [Locale('en'), Locale('ko')],
fallbackLocale: const Locale('en'),
startLocale: const Locale('en'),
path: 'assets/translations',
assetLoader: const _TestAssetLoader(),
child: const ProviderScope(child: BaronSSOApp()),
),
);
await tester.pump(); // 한 프레임 더
});
}