🌐 Localized files

This commit is contained in:
2025-11-23 01:43:54 +08:00
parent f9a09599c9
commit 108a6da074
11 changed files with 288 additions and 124 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
@@ -33,7 +34,7 @@ class FileManagementActionSection extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'File Actions',
'fileActions'.tr(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -45,10 +46,8 @@ class FileManagementActionSection extends HookConsumerWidget {
Symbols.delete_forever,
color: theme.colorScheme.error,
),
title: const Text('Purge Files'),
subtitle: const Text(
'Remove all uploaded files from the site',
),
title: Text('purgeFiles'.tr()),
subtitle: Text('purgeFilesDescription'.tr()),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () => _purgeFiles(context, ref),
),
@@ -58,10 +57,8 @@ class FileManagementActionSection extends HookConsumerWidget {
Symbols.upload,
color: theme.colorScheme.primary,
),
title: const Text('Deploy Site'),
subtitle: const Text(
'Upload and deploy a new version from ZIP archive',
),
title: Text('deploySite'.tr()),
subtitle: Text('deploySiteDescription'.tr()),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () => _deploySite(context, ref),
),
@@ -79,21 +76,19 @@ class FileManagementActionSection extends HookConsumerWidget {
context: context,
builder:
(context) => AlertDialog(
title: const Text('Confirm Purge'),
content: const Text(
'This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?',
),
title: Text('confirmPurge'.tr()),
content: Text('purgeFilesConfirm'.tr()),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
child: Text('cancel'.tr()),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('Purge All Files'),
child: Text('purgeAllFiles'.tr()),
),
],
),
@@ -105,13 +100,13 @@ class FileManagementActionSection extends HookConsumerWidget {
final apiClient = ref.read(apiClientProvider);
await apiClient.delete('/zone/sites/${site.id}/files/purge');
if (context.mounted) {
showSnackBar('All files purged successfully');
showSnackBar('allFilesPurgedSuccess'.tr());
// Refresh the file management section
ref.invalidate(siteFilesProvider(siteId: site.id));
}
} catch (e) {
if (context.mounted) {
showSnackBar('Failed to purge files: $e');
showSnackBar('failedToPurgeFiles'.tr(args: [e.toString()]));
}
}
}
@@ -147,13 +142,13 @@ class FileManagementActionSection extends HookConsumerWidget {
);
if (context.mounted) {
showSnackBar('Site deployed successfully');
showSnackBar('siteDeployedSuccess'.tr());
// Refresh the file management section
ref.invalidate(siteFilesProvider(siteId: site.id));
}
} catch (e) {
if (context.mounted) {
showSnackBar('Failed to deploy site: $e');
showSnackBar('failedToDeploySite'.tr(args: [e.toString()]));
}
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -42,7 +43,7 @@ class FileManagementSection extends HookConsumerWidget {
Icon(Symbols.folder, size: 20),
const Gap(8),
Text(
'File Management',
'fileManagement'.tr(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -75,9 +76,7 @@ class FileManagementSection extends HookConsumerWidget {
files =
results.map((m) => m['file'] as File).toList();
if (files.isEmpty) {
showSnackBar(
'No files found in the selected folder',
);
showSnackBar('noFilesFoundInFolder'.tr());
return;
}
}
@@ -112,23 +111,23 @@ class FileManagementSection extends HookConsumerWidget {
},
itemBuilder:
(BuildContext context) => [
const PopupMenuItem<String>(
PopupMenuItem<String>(
value: 'files',
child: Row(
children: [
Icon(Symbols.file_copy),
Gap(12),
Text('Files'),
Text('siteFiles'.tr()),
],
),
),
const PopupMenuItem<String>(
PopupMenuItem<String>(
value: 'folder',
child: Row(
children: [
Icon(Symbols.folder),
Gap(12),
Text('Folder'),
Text('siteFolder'.tr()),
],
),
),
@@ -182,7 +181,7 @@ class FileManagementSection extends HookConsumerWidget {
children: [
InkWell(
onTap: () => currentPath.value = null,
child: const Text('Root'),
child: Text('siteRoot'.tr()),
),
...() {
final parts =
@@ -230,12 +229,12 @@ class FileManagementSection extends HookConsumerWidget {
),
const Gap(16),
Text(
'No files uploaded yet',
'noFilesUploadedYet'.tr(),
style: theme.textTheme.bodyLarge,
),
const Gap(8),
Text(
'Upload your first file to get started',
'uploadFirstFile'.tr(),
style: theme.textTheme.bodySmall,
),
],
@@ -265,7 +264,7 @@ class FileManagementSection extends HookConsumerWidget {
(error, stack) => Center(
child: Column(
children: [
Text('Failed to load files'),
Text('failedToLoadFiles'.tr()),
const Gap(8),
ElevatedButton(
onPressed:
@@ -275,7 +274,7 @@ class FileManagementSection extends HookConsumerWidget {
path: currentPath.value,
),
),
child: const Text('Retry'),
child: Text('retry'.tr()),
),
],
),

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -29,7 +30,7 @@ class PagesSection extends HookConsumerWidget {
const Icon(Symbols.article, size: 20),
const Gap(8),
Text(
'Pages',
'sitePages'.tr(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -72,12 +73,12 @@ class PagesSection extends HookConsumerWidget {
),
const Gap(16),
Text(
'No pages yet',
'noPagesYet'.tr(),
style: theme.textTheme.bodyLarge,
),
const Gap(8),
Text(
'Create your first page to get started',
'createFirstPage'.tr(),
style: theme.textTheme.bodySmall,
),
],
@@ -101,14 +102,14 @@ class PagesSection extends HookConsumerWidget {
(error, stack) => Center(
child: Column(
children: [
Text('Failed to load pages'),
Text('failedToLoadPages'.tr()),
const Gap(8),
ElevatedButton(
onPressed:
() => ref.invalidate(
sitePagesProvider(pubName, site.slug),
),
child: const Text('Retry'),
child: Text('retry'.tr()),
),
],
),

View File

@@ -64,18 +64,16 @@ class SiteActionMenu extends HookConsumerWidget {
context: context,
builder:
(context) => AlertDialog(
title: const Text('Delete Site'),
content: const Text(
'Are you sure you want to delete this publication site? This action cannot be undone.',
),
title: Text('deleteSite'.tr()),
content: Text('publicationSiteDeleteConfirm'.tr()),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
child: Text('cancel'.tr()),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Delete'),
child: Text('delete'.tr()),
),
],
),
@@ -86,7 +84,7 @@ class SiteActionMenu extends HookConsumerWidget {
final client = ref.read(apiClientProvider);
await client.delete('/zone/sites/${site.id}');
if (context.mounted) {
showSnackBar('Site deleted successfully');
showSnackBar('siteDeletedSuccess'.tr());
Navigator.of(context).pop();
}
} catch (e) {

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -42,20 +43,20 @@ class SiteDetailContent extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Site Information',
'siteInformation'.tr(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Gap(16),
InfoRow(
label: 'Name',
label: 'name'.tr(),
value: site.name,
icon: Symbols.title,
),
const Gap(8),
InfoRow(
label: 'Slug',
label: 'slug'.tr(),
value: site.slug,
icon: Symbols.tag,
monospace: true,
@@ -63,27 +64,30 @@ class SiteDetailContent extends HookConsumerWidget {
const Gap(8),
InfoRow(
label: 'Mode',
value: site.mode == 0 ? 'Fully Managed' : 'Self-Managed',
value:
site.mode == 0
? 'siteModeFullyManaged'.tr()
: 'siteModeSelfManaged'.tr(),
icon: Symbols.settings,
),
if (site.description != null &&
site.description!.isNotEmpty) ...[
const Gap(8),
InfoRow(
label: 'Description',
label: 'description'.tr(),
value: site.description!,
icon: Symbols.description,
),
],
const Gap(8),
InfoRow(
label: 'Created',
label: 'siteCreated'.tr(),
value: site.createdAt.formatSystem(),
icon: Symbols.calendar_add_on,
),
const Gap(8),
InfoRow(
label: 'Updated',
label: 'siteUpdated'.tr(),
value: site.updatedAt.formatSystem(),
icon: Symbols.update,
),

View File

@@ -57,7 +57,7 @@ class FundEnvelopeWidget extends HookConsumerWidget {
),
const SizedBox(height: 8),
Text(
'Failed to load fund envelope',
'fundEnvelopeLoadFailed'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
@@ -88,7 +88,7 @@ class FundEnvelopeWidget extends HookConsumerWidget {
const SizedBox(width: 8),
Expanded(
child: Text(
'Fund Envelope',
'fundEnvelope'.tr(),
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
),
@@ -116,7 +116,12 @@ class FundEnvelopeWidget extends HookConsumerWidget {
const SizedBox(height: 4),
if (fund.remainingAmount != fund.totalAmount)
Text(
'Remaining: ${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency}',
'fundEnvelopeRemaining'.tr(
args: [
fund.remainingAmount.toStringAsFixed(2),
fund.currency,
],
),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
@@ -126,7 +131,13 @@ class FundEnvelopeWidget extends HookConsumerWidget {
),
),
Text(
'Split: ${fund.splitType == 0 ? 'Evenly' : 'Randomly'}',
'fundEnvelopeSplit'.tr(
args: [
fund.splitType == 0
? 'fundEnvelopeSplitEvenly'.tr()
: 'fundEnvelopeSplitRandomly'.tr(),
],
),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
@@ -245,7 +256,7 @@ class FundEnvelopeWidget extends HookConsumerWidget {
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
showSnackBar('Fund claimed successfully!');
showSnackBar('fundEnvelopeClaimSuccess'.tr());
}
} catch (e) {
showErrorAlert(e);
@@ -261,23 +272,23 @@ class FundEnvelopeWidget extends HookConsumerWidget {
switch (status) {
case 0:
text = 'Created';
text = 'fundEnvelopeStatusCreated'.tr();
color = Colors.blue;
break;
case 1:
text = 'Partially Claimed';
text = 'fundEnvelopeStatusPartial'.tr();
color = Colors.orange;
break;
case 2:
text = 'Fully Claimed';
text = 'fundEnvelopeStatusCompleted'.tr();
color = Colors.green;
break;
case 3:
text = 'Expired';
text = 'fundEnvelopeStatusExpired'.tr();
color = Colors.red;
break;
default:
text = 'Unknown';
text = 'fundEnvelopeStatusUnknown'.tr();
color = Colors.grey;
}
@@ -339,7 +350,9 @@ class FundEnvelopeWidget extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recipients ($claimedCount/$totalCount claimed)',
'fundEnvelopeRecipients'.tr(
args: [claimedCount.toString(), totalCount.toString()],
),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
@@ -362,15 +375,26 @@ class FundEnvelopeWidget extends HookConsumerWidget {
final difference = date.difference(now);
if (difference.isNegative) {
return 'Expired ${difference.inDays.abs()} days ago';
final days = difference.inDays.abs();
return 'fundEnvelopeExpiredDaysAgo'.plural(
days,
args: [days.toString()],
);
} else if (difference.inDays == 0) {
final hours = difference.inHours;
if (hours == 0) {
return 'Expires soon';
return 'fundEnvelopeExpiresSoon'.tr();
}
return 'Expires in $hours hour${hours == 1 ? '' : 's'}';
return 'fundEnvelopeExpiresInHours'.plural(
hours,
args: [hours.toString()],
);
} else if (difference.inDays < 7) {
return 'Expires in ${difference.inDays} day${difference.inDays == 1 ? '' : 's'}';
final days = difference.inDays;
return 'fundEnvelopeExpiresInDays'.plural(
days,
args: [days.toString()],
);
} else {
return '${date.day}/${date.month}/${date.year}';
}
@@ -449,7 +473,13 @@ class FundClaimDialog extends HookConsumerWidget {
// Remaining amount
Text(
'${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency} / $remainingSplits splits',
'fundEnvelopeRemainingWithSplits'.tr(
args: [
fund.remainingAmount.toStringAsFixed(2),
fund.currency,
remainingSplits.toString(),
],
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.w500,
@@ -503,7 +533,8 @@ class FundClaimDialog extends HookConsumerWidget {
const SizedBox(width: 8),
Expanded(
child: Text(
recipient.recipientAccount?.nick ?? 'Unknown User',
recipient.recipientAccount?.nick ??
'fundEnvelopeUnknownUser'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
@@ -551,7 +582,8 @@ class FundClaimDialog extends HookConsumerWidget {
const SizedBox(width: 8),
Expanded(
child: Text(
recipient.recipientAccount?.nick ?? 'Unknown User',
recipient.recipientAccount?.nick ??
'fundEnvelopeUnknownUser'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
),