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,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;
}

View 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);
}

View File

@@ -0,0 +1,11 @@
class LocaleStorageImpl {
String? _locale;
String? read() => _locale;
void write(String locale) {
_locale = locale;
}
}
final localeStorage = LocaleStorageImpl();

View 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();

View 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();
}

View 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 {};
}
}
}