forked from baron/baron-sso
feat: i18n 개선 및 userfront 로그인/로케일 보완
This commit is contained in:
51
userfront/lib/core/i18n/locale_gate.dart
Normal file
51
userfront/lib/core/i18n/locale_gate.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:easy_localization/easy_localization.dart' hide tr;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:userfront/i18n.dart';
|
||||
import '../services/web_window.dart';
|
||||
import 'locale_storage.dart';
|
||||
import 'locale_utils.dart';
|
||||
|
||||
class LocaleGate extends StatefulWidget {
|
||||
const LocaleGate({super.key, required this.localeCode, required this.child});
|
||||
|
||||
final String localeCode;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<LocaleGate> createState() => _LocaleGateState();
|
||||
}
|
||||
|
||||
class _LocaleGateState extends State<LocaleGate> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_applyLocale();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LocaleGate oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.localeCode != widget.localeCode) {
|
||||
_applyLocale();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyLocale() async {
|
||||
final normalized = normalizeLocaleCode(widget.localeCode);
|
||||
LocaleStorage.write(normalized);
|
||||
webWindow.setTitle(
|
||||
tr('ui.userfront.app_title'),
|
||||
);
|
||||
if (context.locale.languageCode == normalized) {
|
||||
return;
|
||||
}
|
||||
await context.setLocale(Locale(normalized));
|
||||
webWindow.setTitle(
|
||||
tr('ui.userfront.app_title'),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
7
userfront/lib/core/i18n/locale_storage.dart
Normal file
7
userfront/lib/core/i18n/locale_storage.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'locale_storage_stub.dart'
|
||||
if (dart.library.html) 'locale_storage_web.dart';
|
||||
|
||||
abstract class LocaleStorage {
|
||||
static String? read() => localeStorage.read();
|
||||
static void write(String locale) => localeStorage.write(locale);
|
||||
}
|
||||
11
userfront/lib/core/i18n/locale_storage_stub.dart
Normal file
11
userfront/lib/core/i18n/locale_storage_stub.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
class LocaleStorageImpl {
|
||||
String? _locale;
|
||||
|
||||
String? read() => _locale;
|
||||
|
||||
void write(String locale) {
|
||||
_locale = locale;
|
||||
}
|
||||
}
|
||||
|
||||
final localeStorage = LocaleStorageImpl();
|
||||
120
userfront/lib/core/i18n/locale_storage_web.dart
Normal file
120
userfront/lib/core/i18n/locale_storage_web.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:html' as html;
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class LocaleStorageImpl {
|
||||
static const _key = 'locale';
|
||||
static const _legacyKey = 'baron_locale';
|
||||
static final Map<String, String> _memory = {};
|
||||
static bool _forceMemory = false;
|
||||
static bool _forceSession = false;
|
||||
|
||||
@visibleForTesting
|
||||
static void forceMemoryStorageForTests(bool value) {
|
||||
_forceMemory = value;
|
||||
if (!value) {
|
||||
_memory.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void forceSessionStorageForTests(bool value) {
|
||||
_forceSession = value;
|
||||
}
|
||||
|
||||
String? _read(String key) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
return html.window.localStorage[key];
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
return html.window.sessionStorage[key];
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
return html.window.sessionStorage[key];
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
return _memory[key];
|
||||
}
|
||||
|
||||
void _write(String key, String value) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
html.window.localStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
html.window.sessionStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
html.window.sessionStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
_memory[key] = value;
|
||||
}
|
||||
|
||||
void _remove(String key) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
html.window.localStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
html.window.sessionStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
html.window.sessionStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
_memory.remove(key);
|
||||
}
|
||||
|
||||
String? read() {
|
||||
final current = _read(_key);
|
||||
if (current != null && current.isNotEmpty) {
|
||||
return current;
|
||||
}
|
||||
final legacy = _read(_legacyKey);
|
||||
if (legacy != null && legacy.isNotEmpty) {
|
||||
_write(_key, legacy);
|
||||
_remove(_legacyKey);
|
||||
return legacy;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void write(String locale) {
|
||||
_write(_key, locale);
|
||||
}
|
||||
}
|
||||
|
||||
final localeStorage = LocaleStorageImpl();
|
||||
69
userfront/lib/core/i18n/locale_utils.dart
Normal file
69
userfront/lib/core/i18n/locale_utils.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'locale_storage.dart';
|
||||
|
||||
const supportedLocaleCodes = ['en', 'ko'];
|
||||
const defaultLocaleCode = 'en';
|
||||
|
||||
String normalizeLocaleCode(String? code) {
|
||||
if (code == null || code.isEmpty) {
|
||||
return defaultLocaleCode;
|
||||
}
|
||||
final normalized = code.toLowerCase();
|
||||
if (normalized == 'ko' || normalized.startsWith('ko-')) {
|
||||
return 'ko';
|
||||
}
|
||||
if (normalized == 'en' || normalized.startsWith('en-')) {
|
||||
return 'en';
|
||||
}
|
||||
return defaultLocaleCode;
|
||||
}
|
||||
|
||||
String resolvePreferredLocaleCode() {
|
||||
final stored = LocaleStorage.read();
|
||||
if (stored != null && supportedLocaleCodes.contains(stored)) {
|
||||
return stored;
|
||||
}
|
||||
final deviceLocale = PlatformDispatcher.instance.locale;
|
||||
return normalizeLocaleCode(deviceLocale.languageCode);
|
||||
}
|
||||
|
||||
String? extractLocaleFromPath(Uri uri) {
|
||||
if (uri.pathSegments.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final code = uri.pathSegments.first.toLowerCase();
|
||||
if (supportedLocaleCodes.contains(code)) {
|
||||
return code;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String stripLocalePath(Uri uri) {
|
||||
final segments = uri.pathSegments;
|
||||
if (segments.isNotEmpty && supportedLocaleCodes.contains(segments.first)) {
|
||||
final rest = segments.skip(1).join('/');
|
||||
if (rest.isEmpty) {
|
||||
return '/';
|
||||
}
|
||||
return '/$rest';
|
||||
}
|
||||
return uri.path;
|
||||
}
|
||||
|
||||
String buildLocalizedPath(String localeCode, Uri uri) {
|
||||
final segments = uri.pathSegments;
|
||||
Iterable<String> restSegments = segments;
|
||||
if (segments.isNotEmpty) {
|
||||
final head = segments.first.toLowerCase();
|
||||
if (supportedLocaleCodes.contains(head) || head.length == 2) {
|
||||
restSegments = segments.skip(1);
|
||||
}
|
||||
}
|
||||
final newSegments = [localeCode, ...restSegments];
|
||||
final path = '/${newSegments.join('/')}';
|
||||
if (uri.queryParameters.isEmpty) {
|
||||
return path;
|
||||
}
|
||||
return Uri(path: path, queryParameters: uri.queryParameters).toString();
|
||||
}
|
||||
22
userfront/lib/core/i18n/toml_asset_loader.dart
Normal file
22
userfront/lib/core/i18n/toml_asset_loader.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:toml/toml.dart';
|
||||
|
||||
class TomlAssetLoader extends AssetLoader {
|
||||
const TomlAssetLoader();
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> load(String path, Locale locale) async {
|
||||
final assetPath = '$path/${locale.languageCode}.toml';
|
||||
try {
|
||||
final content = await rootBundle.loadString(assetPath);
|
||||
final document = TomlDocument.parse(content);
|
||||
return document.toMap();
|
||||
} catch (e) {
|
||||
// 로딩 실패 시 빈 맵을 반환해 렌더링을 지속합니다.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user