🌐 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

@@ -1343,5 +1343,133 @@
"clearCompleted": "Clear Completed", "clearCompleted": "Clear Completed",
"contentCantEmpty": "Content cannot be empty", "contentCantEmpty": "Content cannot be empty",
"features": "Features", "features": "Features",
"unnamed": "Unnamed" "unnamed": "Unnamed",
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
"fundEnvelope": "Fund Envelope",
"fundEnvelopeRemaining": "Remaining: {} {}",
"fundEnvelopeSplit": "Split: {}",
"fundEnvelopeSplitEvenly": "Evenly",
"fundEnvelopeSplitRandomly": "Randomly",
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
"fundEnvelopeStatusCreated": "Created",
"fundEnvelopeStatusPartial": "Partially Claimed",
"fundEnvelopeStatusCompleted": "Fully Claimed",
"fundEnvelopeStatusExpired": "Expired",
"fundEnvelopeStatusUnknown": "Unknown",
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
"fundEnvelopeExpiredDaysAgo": {
"one": "Expired {} day ago",
"other": "Expired {} days ago"
},
"fundEnvelopeExpiresSoon": "Expires soon",
"fundEnvelopeExpiresInHours": {
"one": "Expires in {} hour",
"other": "Expires in {} hours"
},
"fundEnvelopeExpiresInDays": {
"one": "Expires in {} day",
"other": "Expires in {} days"
},
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
"fundEnvelopeUnknownUser": "Unknown User",
"deleteSite": "Delete Site",
"deleteSiteConfirm": "Are you sure you want to delete this site?",
"siteDeletedSuccess": "Site deleted successfully",
"siteSlug": "Slug",
"siteSlugHint": "my-site",
"siteSlugRequired": "Please enter a slug",
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
"siteName": "Site Name",
"siteNameHint": "My Publication Site",
"siteNameRequired": "Please enter a site name",
"siteMode": "Mode",
"siteModeFullyManaged": "Fully Managed",
"siteModeSelfManaged": "Self-Managed",
"editPublicationSite": "Edit Publication Site",
"deletePublicationSite": "Delete Publication Site",
"publicationSiteSavedSuccess": "Publication site saved successfully",
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
"newPublicationSite": "New Publication Site",
"siteDetails": "Site Details",
"siteInformation": "Site Information",
"siteDomain": "Domain",
"siteCreated": "Created",
"siteUpdated": "Updated",
"failedToLoadSite": "Failed to load site",
"sitePages": "Pages",
"noPagesYet": "No pages yet",
"createFirstPage": "Create your first page to get started",
"failedToLoadPages": "Failed to load pages",
"fileManagement": "File Management",
"siteFiles": "Files",
"siteFolder": "Folder",
"siteRoot": "Root",
"noFilesUploadedYet": "No files uploaded yet",
"uploadFirstFile": "Upload your first file to get started",
"failedToLoadFiles": "Failed to load files",
"noFilesFoundInFolder": "No files found in the selected folder",
"fileActions": "File Actions",
"purgeFiles": "Purge Files",
"purgeFilesDescription": "Remove all uploaded files from the site",
"deploySite": "Deploy Site",
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
"confirmPurge": "Confirm Purge",
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
"purgeAllFiles": "Purge All Files",
"allFilesPurgedSuccess": "All files purged successfully",
"failedToPurgeFiles": "Failed to purge files: {}",
"siteDeployedSuccess": "Site deployed successfully",
"failedToDeploySite": "Failed to deploy site: {}",
"createPage": "Create Page",
"editPage": "Edit Page",
"pageType": "Page Type",
"htmlPage": "HTML Page",
"redirectPage": "Redirect Page",
"pageTypeRequired": "Please select a page type",
"pagePath": "Page Path",
"pagePathHint": "/about, /contact, etc.",
"pagePathRequired": "Please enter a page path",
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
"pagePathMustStartWithSlash": "Page path must start with /",
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
"pageTitle": "Page Title",
"pageTitleHint": "About Us, Contact, etc.",
"pageTitleRequired": "Please enter a page title",
"pageContentHtml": "Page Content (HTML)",
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
"pageContentRequired": "Please enter HTML content for the page",
"redirectTarget": "Redirect Target",
"redirectTargetHint": "/new-page, https://example.com, etc.",
"redirectTargetRequired": "Please enter a redirect target",
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
"deletePage": "Delete Page",
"deletePageConfirm": "Are you sure you want to delete this page?",
"savePage": "Save Page",
"pageCreatedSuccess": "Page created successfully",
"pageUpdatedSuccess": "Page updated successfully",
"pageDeletedSuccess": "Page deleted successfully",
"uploadFiles": "Upload Files",
"uploadPath": "Upload Path",
"uploadPathHint": "/ (root) or /assets/images/",
"uploadPathRequired": "Please enter an upload path",
"uploadPathMustStartWithSlash": "Path must start with /",
"uploadPathNoSpaces": "Path cannot contain spaces",
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
"percentCompleted": "{}% completed",
"filesToUpload": "{} files to upload",
"fileSizeKb": "Size: {} KB",
"uploadingEllipsis": "Uploading...",
"uploadFilesCount": {
"one": "Upload {} File",
"other": "Upload {} Files"
},
"allUploadsCompleted": "All uploads completed",
"someUploadsFailed": "Some uploads failed",
"uploadingInProgress": "Uploading in progress",
"readyToUpload": "Ready to upload",
"allFilesUploadedSuccess": "All files uploaded successfully",
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
"lotteryMultiplierRequired": "Please enter a multiplier",
"lotteryMultiplierRange": "Multiplier must be between 1 and 10"
} }

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -54,7 +55,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
title: siteAsync.maybeWhen( title: siteAsync.maybeWhen(
data: (site) => Text(site.name), data: (site) => Text(site.name),
orElse: () => const Text('Site Details'), orElse: () => Text('siteDetails'.tr()),
), ),
actions: [ actions: [
siteAsync.maybeWhen( siteAsync.maybeWhen(
@@ -105,26 +106,26 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Site Information', 'siteInformation'.tr(),
style: theme.textTheme.titleMedium style: theme.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold), ?.copyWith(fontWeight: FontWeight.bold),
), ),
const Gap(16), const Gap(16),
InfoRow( InfoRow(
label: 'Name', label: 'name'.tr(),
value: site.name, value: site.name,
icon: Symbols.title, icon: Symbols.title,
), ),
const Gap(8), const Gap(8),
InfoRow( InfoRow(
label: 'Slug', label: 'slug'.tr(),
value: site.slug, value: site.slug,
icon: Symbols.tag, icon: Symbols.tag,
monospace: true, monospace: true,
), ),
const Gap(8), const Gap(8),
InfoRow( InfoRow(
label: 'Domain', label: 'siteDomain'.tr(),
value: '${site.slug}.solian.page', value: '${site.slug}.solian.page',
icon: Symbols.globe, icon: Symbols.globe,
monospace: true, monospace: true,
@@ -136,31 +137,31 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
), ),
const Gap(8), const Gap(8),
InfoRow( InfoRow(
label: 'Mode', label: 'siteMode'.tr(),
value: value:
site.mode == 0 site.mode == 0
? 'Fully Managed' ? 'siteModeFullyManaged'.tr()
: 'Self-Managed', : 'siteModeSelfManaged'.tr(),
icon: Symbols.settings, icon: Symbols.settings,
), ),
if (site.description != null && if (site.description != null &&
site.description!.isNotEmpty) ...[ site.description!.isNotEmpty) ...[
const Gap(8), const Gap(8),
InfoRow( InfoRow(
label: 'Description', label: 'description'.tr(),
value: site.description!, value: site.description!,
icon: Symbols.description, icon: Symbols.description,
), ),
], ],
const Gap(8), const Gap(8),
InfoRow( InfoRow(
label: 'Created', label: 'siteCreated'.tr(),
value: site.createdAt.formatSystem(), value: site.createdAt.formatSystem(),
icon: Symbols.calendar_add_on, icon: Symbols.calendar_add_on,
), ),
const Gap(8), const Gap(8),
InfoRow( InfoRow(
label: 'Updated', label: 'siteUpdated'.tr(),
value: site.updatedAt.formatSystem(), value: site.updatedAt.formatSystem(),
icon: Symbols.update, icon: Symbols.update,
), ),
@@ -191,7 +192,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'Failed to load site', 'failedToLoadSite'.tr(),
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
const Gap(16), const Gap(16),
@@ -202,7 +203,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
() => ref.invalidate( () => ref.invalidate(
publicationSiteDetailProvider(pubName, siteSlug), publicationSiteDetailProvider(pubName, siteSlug),
), ),
child: const Text('Retry'), child: Text('retry'.tr()),
), ),
], ],
), ),

View File

@@ -31,20 +31,20 @@ class SiteForm extends HookConsumerWidget {
children: [ children: [
TextFormField( TextFormField(
controller: slugController, controller: slugController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Slug', labelText: 'siteSlug'.tr(),
hintText: 'my-site', hintText: 'siteSlugHint'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Please enter a slug'; return 'siteSlugRequired'.tr();
} }
final slugRegex = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$'); final slugRegex = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$');
if (!slugRegex.hasMatch(value)) { if (!slugRegex.hasMatch(value)) {
return 'Slug can only contain lowercase letters, numbers, and dashes'; return 'siteSlugInvalid'.tr();
} }
return null; return null;
}, },
@@ -53,16 +53,16 @@ class SiteForm extends HookConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: nameController, controller: nameController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Site Name', labelText: 'siteName'.tr(),
hintText: 'My Publication Site', hintText: 'siteNameHint'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Please enter a site name'; return 'siteNameRequired'.tr();
} }
return null; return null;
}, },
@@ -71,8 +71,8 @@ class SiteForm extends HookConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: descriptionController, controller: descriptionController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Description', labelText: 'description'.tr(),
alignLabelWithHint: true, alignLabelWithHint: true,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
@@ -84,15 +84,18 @@ class SiteForm extends HookConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
DropdownButtonFormField<int>( DropdownButtonFormField<int>(
value: modeController.value, value: modeController.value,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Mode', labelText: 'siteMode'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
), ),
items: const [ items: [
DropdownMenuItem(value: 0, child: Text('Fully Managed')), DropdownMenuItem(
DropdownMenuItem(value: 1, child: Text('Self-Managed')), value: 0,
child: Text('siteModeFullyManaged'.tr()),
),
DropdownMenuItem(value: 1, child: Text('siteModeSelfManaged'.tr())),
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
@@ -104,7 +107,7 @@ class SiteForm extends HookConsumerWidget {
).padding(all: 20); ).padding(all: 20);
return SheetScaffold( return SheetScaffold(
titleText: 'Edit Publication Site', titleText: 'editPublicationSite'.tr(),
child: Builder( child: Builder(
builder: builder:
(context) => SingleChildScrollView( (context) => SingleChildScrollView(
@@ -116,7 +119,7 @@ class SiteForm extends HookConsumerWidget {
TextButton.icon( TextButton.icon(
onPressed: deleteSite, onPressed: deleteSite,
icon: const Icon(Symbols.delete_forever), icon: const Icon(Symbols.delete_forever),
label: const Text('Delete Publication Site'), label: Text('deletePublicationSite'.tr()),
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Colors.red, foregroundColor: Colors.red,
), ),
@@ -171,7 +174,7 @@ class SiteForm extends HookConsumerWidget {
ref.invalidate(siteListNotifierProvider(pubName)); ref.invalidate(siteListNotifierProvider(pubName));
if (context.mounted) { if (context.mounted) {
showSnackBar('Publication site saved successfully'); showSnackBar('publicationSiteSavedSuccess'.tr());
Navigator.pop(context); Navigator.pop(context);
} }
} catch (e) { } catch (e) {
@@ -185,8 +188,8 @@ class SiteForm extends HookConsumerWidget {
if (siteSlug == null) return; // Shouldn't happen for editing if (siteSlug == null) return; // Shouldn't happen for editing
final confirmed = await showConfirmAlert( final confirmed = await showConfirmAlert(
'Are you sure you want to delete this publication site? This action cannot be undone.', 'publicationSiteDeleteConfirm'.tr(),
'Delete Publication Site', 'deletePublicationSite'.tr(),
); );
if (confirmed != true) return; if (confirmed != true) return;
@@ -199,7 +202,7 @@ class SiteForm extends HookConsumerWidget {
ref.invalidate(siteListNotifierProvider(pubName)); ref.invalidate(siteListNotifierProvider(pubName));
if (context.mounted) { if (context.mounted) {
showSnackBar('Publication site deleted successfully'); showSnackBar('publicationSiteDeletedSuccess'.tr());
Navigator.pop(context); Navigator.pop(context);
} }
} catch (e) { } catch (e) {
@@ -243,13 +246,13 @@ class SiteForm extends HookConsumerWidget {
editingSiteSlug, editingSiteSlug,
), ),
loading: loading:
() => const SheetScaffold( () => SheetScaffold(
titleText: 'Edit Publication Site', titleText: 'editPublicationSite'.tr(),
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
), ),
error: error:
(error, _) => SheetScaffold( (error, _) => SheetScaffold(
titleText: 'Edit Publication Site', titleText: 'editPublicationSite'.tr(),
child: ResponseErrorWidget( child: ResponseErrorWidget(
error: error.toString(), error: error.toString(),
onRetry: () { onRetry: () {
@@ -327,9 +330,12 @@ class SiteForm extends HookConsumerWidget {
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
), ),
items: const [ items: [
DropdownMenuItem(value: 0, child: Text('Fully Managed')), DropdownMenuItem(
DropdownMenuItem(value: 1, child: Text('Self-Managed')), value: 0,
child: Text('siteModeFullyManaged'.tr()),
),
DropdownMenuItem(value: 1, child: Text('siteModeSelfManaged'.tr())),
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
@@ -348,7 +354,9 @@ class SiteForm extends HookConsumerWidget {
return SheetScaffold( return SheetScaffold(
titleText: titleText:
siteSlug == null ? 'New Publication Site' : 'Edit Publication Site', siteSlug == null
? 'newPublicationSite'.tr()
: 'editPublicationSite'.tr(),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@@ -359,7 +367,7 @@ class SiteForm extends HookConsumerWidget {
TextButton.icon( TextButton.icon(
onPressed: isLoading.value ? null : deleteSite, onPressed: isLoading.value ? null : deleteSite,
icon: const Icon(Symbols.delete_forever), icon: const Icon(Symbols.delete_forever),
label: const Text('Delete Publication Site'), label: Text('deletePublicationSite'.tr()),
style: TextButton.styleFrom(foregroundColor: Colors.red), style: TextButton.styleFrom(foregroundColor: Colors.red),
).alignment(Alignment.centerRight), ).alignment(Alignment.centerRight),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@@ -74,7 +74,7 @@ class CreatorSiteListScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold( return AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar(title: Text('Publication Sites')), appBar: AppBar(title: Text('publicationSites'.tr())),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => _createSite(context), onPressed: () => _createSite(context),
child: Icon(Icons.add), child: Icon(Icons.add),
@@ -201,21 +201,19 @@ class _CreatorSiteItem extends HookConsumerWidget {
context: context, context: context,
builder: builder:
(context) => AlertDialog( (context) => AlertDialog(
title: Text('Delete Site'), title: Text('deleteSite'.tr()),
content: Text( content: Text('deleteSiteConfirm'.tr()),
'Are you sure you want to delete this site?',
),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed:
() => () =>
Navigator.of(context).pop(false), Navigator.of(context).pop(false),
child: Text('Cancel'), child: Text('cancel'.tr()),
), ),
TextButton( TextButton(
onPressed: onPressed:
() => Navigator.of(context).pop(true), () => Navigator.of(context).pop(true),
child: Text('Delete'), child: Text('delete'.tr()),
), ),
], ],
), ),
@@ -225,7 +223,7 @@ class _CreatorSiteItem extends HookConsumerWidget {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
await client.delete('/zone/sites/${site.id}'); await client.delete('/zone/sites/${site.id}');
ref.invalidate(siteListNotifierProvider(pubName)); ref.invalidate(siteListNotifierProvider(pubName));
showSnackBar('Site deleted successfully'); showSnackBar('siteDeletedSuccess'.tr());
} catch (e) { } catch (e) {
showErrorAlert(e); showErrorAlert(e);
} }

View File

@@ -534,7 +534,7 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
), ),
const Gap(4), const Gap(4),
Text( Text(
'The last selected number will be your special number.', 'lotteryLastNumberSpecial'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -738,11 +738,11 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
}, },
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Please enter a multiplier'; return 'lotteryMultiplierRequired'.tr();
} }
final parsed = int.tryParse(value); final parsed = int.tryParse(value);
if (parsed == null || parsed < 1 || parsed > 10) { if (parsed == null || parsed < 1 || parsed > 10) {
return 'Multiplier must be between 1 and 10'; return 'lotteryMultiplierRange'.tr();
} }
return null; return null;
}, },

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@@ -33,7 +34,7 @@ class FileManagementActionSection extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'File Actions', 'fileActions'.tr(),
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -45,10 +46,8 @@ class FileManagementActionSection extends HookConsumerWidget {
Symbols.delete_forever, Symbols.delete_forever,
color: theme.colorScheme.error, color: theme.colorScheme.error,
), ),
title: const Text('Purge Files'), title: Text('purgeFiles'.tr()),
subtitle: const Text( subtitle: Text('purgeFilesDescription'.tr()),
'Remove all uploaded files from the site',
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () => _purgeFiles(context, ref), onTap: () => _purgeFiles(context, ref),
), ),
@@ -58,10 +57,8 @@ class FileManagementActionSection extends HookConsumerWidget {
Symbols.upload, Symbols.upload,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
title: const Text('Deploy Site'), title: Text('deploySite'.tr()),
subtitle: const Text( subtitle: Text('deploySiteDescription'.tr()),
'Upload and deploy a new version from ZIP archive',
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () => _deploySite(context, ref), onTap: () => _deploySite(context, ref),
), ),
@@ -79,21 +76,19 @@ class FileManagementActionSection extends HookConsumerWidget {
context: context, context: context,
builder: builder:
(context) => AlertDialog( (context) => AlertDialog(
title: const Text('Confirm Purge'), title: Text('confirmPurge'.tr()),
content: const Text( content: Text('purgeFilesConfirm'.tr()),
'This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?',
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'), child: Text('cancel'.tr()),
), ),
FilledButton( FilledButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error, 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); final apiClient = ref.read(apiClientProvider);
await apiClient.delete('/zone/sites/${site.id}/files/purge'); await apiClient.delete('/zone/sites/${site.id}/files/purge');
if (context.mounted) { if (context.mounted) {
showSnackBar('All files purged successfully'); showSnackBar('allFilesPurgedSuccess'.tr());
// Refresh the file management section // Refresh the file management section
ref.invalidate(siteFilesProvider(siteId: site.id)); ref.invalidate(siteFilesProvider(siteId: site.id));
} }
} catch (e) { } catch (e) {
if (context.mounted) { 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) { if (context.mounted) {
showSnackBar('Site deployed successfully'); showSnackBar('siteDeployedSuccess'.tr());
// Refresh the file management section // Refresh the file management section
ref.invalidate(siteFilesProvider(siteId: site.id)); ref.invalidate(siteFilesProvider(siteId: site.id));
} }
} catch (e) { } catch (e) {
if (context.mounted) { 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 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -42,7 +43,7 @@ class FileManagementSection extends HookConsumerWidget {
Icon(Symbols.folder, size: 20), Icon(Symbols.folder, size: 20),
const Gap(8), const Gap(8),
Text( Text(
'File Management', 'fileManagement'.tr(),
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -75,9 +76,7 @@ class FileManagementSection extends HookConsumerWidget {
files = files =
results.map((m) => m['file'] as File).toList(); results.map((m) => m['file'] as File).toList();
if (files.isEmpty) { if (files.isEmpty) {
showSnackBar( showSnackBar('noFilesFoundInFolder'.tr());
'No files found in the selected folder',
);
return; return;
} }
} }
@@ -112,23 +111,23 @@ class FileManagementSection extends HookConsumerWidget {
}, },
itemBuilder: itemBuilder:
(BuildContext context) => [ (BuildContext context) => [
const PopupMenuItem<String>( PopupMenuItem<String>(
value: 'files', value: 'files',
child: Row( child: Row(
children: [ children: [
Icon(Symbols.file_copy), Icon(Symbols.file_copy),
Gap(12), Gap(12),
Text('Files'), Text('siteFiles'.tr()),
], ],
), ),
), ),
const PopupMenuItem<String>( PopupMenuItem<String>(
value: 'folder', value: 'folder',
child: Row( child: Row(
children: [ children: [
Icon(Symbols.folder), Icon(Symbols.folder),
Gap(12), Gap(12),
Text('Folder'), Text('siteFolder'.tr()),
], ],
), ),
), ),
@@ -182,7 +181,7 @@ class FileManagementSection extends HookConsumerWidget {
children: [ children: [
InkWell( InkWell(
onTap: () => currentPath.value = null, onTap: () => currentPath.value = null,
child: const Text('Root'), child: Text('siteRoot'.tr()),
), ),
...() { ...() {
final parts = final parts =
@@ -230,12 +229,12 @@ class FileManagementSection extends HookConsumerWidget {
), ),
const Gap(16), const Gap(16),
Text( Text(
'No files uploaded yet', 'noFilesUploadedYet'.tr(),
style: theme.textTheme.bodyLarge, style: theme.textTheme.bodyLarge,
), ),
const Gap(8), const Gap(8),
Text( Text(
'Upload your first file to get started', 'uploadFirstFile'.tr(),
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall,
), ),
], ],
@@ -265,7 +264,7 @@ class FileManagementSection extends HookConsumerWidget {
(error, stack) => Center( (error, stack) => Center(
child: Column( child: Column(
children: [ children: [
Text('Failed to load files'), Text('failedToLoadFiles'.tr()),
const Gap(8), const Gap(8),
ElevatedButton( ElevatedButton(
onPressed: onPressed:
@@ -275,7 +274,7 @@ class FileManagementSection extends HookConsumerWidget {
path: currentPath.value, 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:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -29,7 +30,7 @@ class PagesSection extends HookConsumerWidget {
const Icon(Symbols.article, size: 20), const Icon(Symbols.article, size: 20),
const Gap(8), const Gap(8),
Text( Text(
'Pages', 'sitePages'.tr(),
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -72,12 +73,12 @@ class PagesSection extends HookConsumerWidget {
), ),
const Gap(16), const Gap(16),
Text( Text(
'No pages yet', 'noPagesYet'.tr(),
style: theme.textTheme.bodyLarge, style: theme.textTheme.bodyLarge,
), ),
const Gap(8), const Gap(8),
Text( Text(
'Create your first page to get started', 'createFirstPage'.tr(),
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall,
), ),
], ],
@@ -101,14 +102,14 @@ class PagesSection extends HookConsumerWidget {
(error, stack) => Center( (error, stack) => Center(
child: Column( child: Column(
children: [ children: [
Text('Failed to load pages'), Text('failedToLoadPages'.tr()),
const Gap(8), const Gap(8),
ElevatedButton( ElevatedButton(
onPressed: onPressed:
() => ref.invalidate( () => ref.invalidate(
sitePagesProvider(pubName, site.slug), sitePagesProvider(pubName, site.slug),
), ),
child: const Text('Retry'), child: Text('retry'.tr()),
), ),
], ],
), ),

View File

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

View File

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

View File

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