diff --git a/assets/data/text.json b/assets/data/text.json index 79890f4..a6de54e 100644 --- a/assets/data/text.json +++ b/assets/data/text.json @@ -11,6 +11,9 @@ "save": "Save", "cancel": "Cancel", "delete": "Delete", + "deleteSubscriptionTitle": "Delete Subscription", + "deleteSubscriptionMessage": "Are you sure you want to delete @ subscription?", + "deleteIrreversibleWarning": "This action cannot be undone", "edit": "Edit", "totalSubscriptions": "Total Subscriptions", "totalMonthlyExpense": "Total Monthly Expense", @@ -267,6 +270,9 @@ "save": "저장", "cancel": "취소", "delete": "삭제", + "deleteSubscriptionTitle": "구독 삭제", + "deleteSubscriptionMessage": "정말로 @ 구독을 삭제하시겠습니까?", + "deleteIrreversibleWarning": "이 작업은 되돌릴 수 없습니다", "edit": "수정", "totalSubscriptions": "총 구독", "totalMonthlyExpense": "이번 달 총 지출", @@ -523,6 +529,9 @@ "save": "保存", "cancel": "キャンセル", "delete": "削除", + "deleteSubscriptionTitle": "サブスクリプション削除", + "deleteSubscriptionMessage": "本当に@のサブスクリプションを削除しますか?", + "deleteIrreversibleWarning": "この操作は取り消せません", "edit": "編集", "totalSubscriptions": "総サブスクリプション", "totalMonthlyExpense": "今月の総支出", @@ -768,6 +777,9 @@ "save": "保存", "cancel": "取消", "delete": "删除", + "deleteSubscriptionTitle": "删除订阅", + "deleteSubscriptionMessage": "确定要删除@订阅吗?", + "deleteIrreversibleWarning": "此操作无法撤销", "edit": "编辑", "totalSubscriptions": "订阅总数", "totalMonthlyExpense": "本月总支出", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2084d83..d208ee0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -36,6 +36,16 @@ class AppLocalizations { String get save => _localizedStrings['save'] ?? 'Save'; String get cancel => _localizedStrings['cancel'] ?? 'Cancel'; String get delete => _localizedStrings['delete'] ?? 'Delete'; + String get deleteSubscriptionTitle => + _localizedStrings['deleteSubscriptionTitle'] ?? 'Delete Subscription'; + String get deleteSubscriptionMessageTemplate => + _localizedStrings['deleteSubscriptionMessage'] ?? + 'Are you sure you want to delete @ subscription?'; + String deleteSubscriptionMessage(String serviceName) => + deleteSubscriptionMessageTemplate.replaceAll('@', serviceName); + String get deleteIrreversibleWarning => + _localizedStrings['deleteIrreversibleWarning'] ?? + 'This action cannot be undone'; String get edit => _localizedStrings['edit'] ?? 'Edit'; String get totalSubscriptions => _localizedStrings['totalSubscriptions'] ?? 'Total Subscriptions'; diff --git a/lib/widgets/analysis/subscription_pie_chart_card.dart b/lib/widgets/analysis/subscription_pie_chart_card.dart index 704d149..9d7786b 100644 --- a/lib/widgets/analysis/subscription_pie_chart_card.dart +++ b/lib/widgets/analysis/subscription_pie_chart_card.dart @@ -233,56 +233,66 @@ class _SubscriptionPieChartCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ThemedText.headline( - text: AppLocalizations.of(context) - .subscriptionServiceRatio, - style: const TextStyle( - fontSize: 18, + Expanded( + child: ThemedText.headline( + text: AppLocalizations.of(context) + .subscriptionServiceRatio, + style: const TextStyle( + fontSize: 18, + ), ), ), - FutureBuilder( - future: CurrencyUtil.getExchangeRateInfoForLocale( - context - .watch() - .locale - .languageCode), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.3), - width: 1, + Flexible( + child: FutureBuilder( + future: CurrencyUtil.getExchangeRateInfoForLocale( + context + .watch() + .locale + .languageCode), + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.data!.isNotEmpty) { + return Padding( + padding: const EdgeInsets.only(left: 12), + child: Align( + alignment: Alignment.topRight, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.3), + width: 1, + ), + ), + child: ThemedText( + AppLocalizations.of(context) + .exchangeRateFormat(snapshot.data!), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.right, + ), + ), ), - ), - child: Text( - AppLocalizations.of(context) - .exchangeRateFormat(snapshot.data!), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: - Theme.of(context).colorScheme.primary, - ), - ), - ); - } - return const SizedBox.shrink(); - }, + ); + } + return const SizedBox.shrink(); + }, + ), ), ], ), diff --git a/lib/widgets/detail/detail_header_section.dart b/lib/widgets/detail/detail_header_section.dart index b423c31..905f453 100644 --- a/lib/widgets/detail/detail_header_section.dart +++ b/lib/widgets/detail/detail_header_section.dart @@ -265,23 +265,39 @@ class DetailHeaderSection extends StatelessWidget { final loc = AppLocalizations.of(context); switch (cycle.toLowerCase()) { case '매월': + case '월간': case 'monthly': case '毎月': + case '月付': + case '月間': case '每月': return loc.billingCycleMonthly; case '분기별': + case '분기': case 'quarterly': + case 'quarter': + case '季付': + case '季度付': case '四半期': case '每季度': return loc.billingCycleQuarterly; case '반기별': case 'half-yearly': + case 'half yearly': + case 'semiannual': + case 'semi-annual': + case '半年付': + case '半年払い': case '半年ごと': case '每半年': return loc.billingCycleHalfYearly; case '매년': + case '연간': case 'yearly': + case 'annual': + case 'annually': case '年間': + case '年付': case '每年': return loc.billingCycleYearly; default: diff --git a/lib/widgets/dialogs/delete_confirmation_dialog.dart b/lib/widgets/dialogs/delete_confirmation_dialog.dart index 2d6eb21..b294eb1 100644 --- a/lib/widgets/dialogs/delete_confirmation_dialog.dart +++ b/lib/widgets/dialogs/delete_confirmation_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; // Material 3 기반 다이얼로그 import '../common/buttons/primary_button.dart'; import '../common/buttons/secondary_button.dart'; +import '../../l10n/app_localizations.dart'; /// 삭제 확인 다이얼로그 /// 글래스모피즘 스타일의 삭제 확인 다이얼로그입니다. @@ -15,8 +16,20 @@ class DeleteConfirmationDialog extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final textThemeColor = Theme.of(context).colorScheme; + final baseMessageStyle = TextStyle( + fontSize: 16, + color: textThemeColor.onSurfaceVariant, + height: 1.5, + ); + final highlightStyle = baseMessageStyle.copyWith( + fontWeight: FontWeight.w600, + color: textThemeColor.onSurface, + ); + return Dialog( - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: textThemeColor.surface, elevation: 6, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), child: Container( @@ -44,11 +57,11 @@ class DeleteConfirmationDialog extends StatelessWidget { // 타이틀 Text( - '구독 삭제', + loc.deleteSubscriptionTitle, style: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, + color: textThemeColor.onSurface, ), ), const SizedBox(height: 12), @@ -57,22 +70,12 @@ class DeleteConfirmationDialog extends StatelessWidget { RichText( textAlign: TextAlign.center, text: TextSpan( - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - height: 1.5, + style: baseMessageStyle, + children: _buildLocalizedMessageSpans( + loc.deleteSubscriptionMessageTemplate, + serviceName, + highlightStyle, ), - children: [ - const TextSpan(text: '정말로 '), - TextSpan( - text: serviceName, - style: TextStyle( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const TextSpan(text: ' 구독을\n삭제하시겠습니까?'), - ], ), ), const SizedBox(height: 8), @@ -84,14 +87,10 @@ class DeleteConfirmationDialog extends StatelessWidget { vertical: 12, ), decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.error.withValues(alpha: 0.05), + color: textThemeColor.error.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( - color: Theme.of(context) - .colorScheme - .error - .withValues(alpha: 0.2), + color: textThemeColor.error.withValues(alpha: 0.2), width: 1, ), ), @@ -100,18 +99,15 @@ class DeleteConfirmationDialog extends StatelessWidget { children: [ Icon( Icons.warning_amber_rounded, - color: Theme.of(context) - .colorScheme - .error - .withValues(alpha: 0.8), + color: textThemeColor.error.withValues(alpha: 0.8), size: 20, ), const SizedBox(width: 8), Text( - '이 작업은 되돌릴 수 없습니다', + loc.deleteIrreversibleWarning, style: TextStyle( fontSize: 14, - color: Theme.of(context).colorScheme.error, + color: textThemeColor.error, fontWeight: FontWeight.w500, ), ), @@ -125,7 +121,7 @@ class DeleteConfirmationDialog extends StatelessWidget { children: [ Expanded( child: SecondaryButton( - text: '취소', + text: loc.cancel, onPressed: () { Navigator.of(context).pop(false); }, @@ -134,12 +130,12 @@ class DeleteConfirmationDialog extends StatelessWidget { const SizedBox(width: 12), Expanded( child: PrimaryButton( - text: '삭제', + text: loc.delete, icon: Icons.delete_rounded, onPressed: () { Navigator.of(context).pop(true); }, - backgroundColor: Theme.of(context).colorScheme.error, + backgroundColor: textThemeColor.error, ), ), ], @@ -166,4 +162,27 @@ class DeleteConfirmationDialog extends StatelessWidget { return result ?? false; } + + List _buildLocalizedMessageSpans( + String template, + String serviceName, + TextStyle highlightStyle, + ) { + final parts = template.split('@'); + if (parts.length == 1) { + return [TextSpan(text: template)]; + } + + final spans = []; + for (int i = 0; i < parts.length; i++) { + final segment = parts[i]; + if (segment.isNotEmpty) { + spans.add(TextSpan(text: segment)); + } + if (i < parts.length - 1) { + spans.add(TextSpan(text: serviceName, style: highlightStyle)); + } + } + return spans; + } }