diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index b4c6004c..b614032a 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1343,5 +1343,133 @@ "clearCompleted": "Clear Completed", "contentCantEmpty": "Content cannot be empty", "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": "

Hello World

This is my page content...

", + "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" } \ No newline at end of file diff --git a/lib/screens/creators/sites/site_detail.dart b/lib/screens/creators/sites/site_detail.dart index 6b27bc2b..e7ab1e81 100644 --- a/lib/screens/creators/sites/site_detail.dart +++ b/lib/screens/creators/sites/site_detail.dart @@ -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'; @@ -54,7 +55,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget { appBar: AppBar( title: siteAsync.maybeWhen( data: (site) => Text(site.name), - orElse: () => const Text('Site Details'), + orElse: () => Text('siteDetails'.tr()), ), actions: [ siteAsync.maybeWhen( @@ -105,26 +106,26 @@ class PublicationSiteDetailScreen 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, ), const Gap(8), InfoRow( - label: 'Domain', + label: 'siteDomain'.tr(), value: '${site.slug}.solian.page', icon: Symbols.globe, monospace: true, @@ -136,31 +137,31 @@ class PublicationSiteDetailScreen extends HookConsumerWidget { ), const Gap(8), InfoRow( - label: 'Mode', + label: 'siteMode'.tr(), value: site.mode == 0 - ? 'Fully Managed' - : 'Self-Managed', + ? '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, ), @@ -191,7 +192,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Failed to load site', + 'failedToLoadSite'.tr(), style: Theme.of(context).textTheme.headlineSmall, ), const Gap(16), @@ -202,7 +203,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget { () => ref.invalidate( publicationSiteDetailProvider(pubName, siteSlug), ), - child: const Text('Retry'), + child: Text('retry'.tr()), ), ], ), diff --git a/lib/screens/creators/sites/site_edit.dart b/lib/screens/creators/sites/site_edit.dart index 6678419a..574e6b00 100644 --- a/lib/screens/creators/sites/site_edit.dart +++ b/lib/screens/creators/sites/site_edit.dart @@ -31,20 +31,20 @@ class SiteForm extends HookConsumerWidget { children: [ TextFormField( controller: slugController, - decoration: const InputDecoration( - labelText: 'Slug', - hintText: 'my-site', + decoration: InputDecoration( + labelText: 'siteSlug'.tr(), + hintText: 'siteSlugHint'.tr(), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), validator: (value) { if (value == null || value.isEmpty) { - return 'Please enter a slug'; + return 'siteSlugRequired'.tr(); } final slugRegex = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$'); if (!slugRegex.hasMatch(value)) { - return 'Slug can only contain lowercase letters, numbers, and dashes'; + return 'siteSlugInvalid'.tr(); } return null; }, @@ -53,16 +53,16 @@ class SiteForm extends HookConsumerWidget { const SizedBox(height: 16), TextFormField( controller: nameController, - decoration: const InputDecoration( - labelText: 'Site Name', - hintText: 'My Publication Site', + decoration: InputDecoration( + labelText: 'siteName'.tr(), + hintText: 'siteNameHint'.tr(), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), validator: (value) { if (value == null || value.isEmpty) { - return 'Please enter a site name'; + return 'siteNameRequired'.tr(); } return null; }, @@ -71,8 +71,8 @@ class SiteForm extends HookConsumerWidget { const SizedBox(height: 16), TextFormField( controller: descriptionController, - decoration: const InputDecoration( - labelText: 'Description', + decoration: InputDecoration( + labelText: 'description'.tr(), alignLabelWithHint: true, border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), @@ -84,15 +84,18 @@ class SiteForm extends HookConsumerWidget { const SizedBox(height: 16), DropdownButtonFormField( value: modeController.value, - decoration: const InputDecoration( - labelText: 'Mode', + decoration: InputDecoration( + labelText: 'siteMode'.tr(), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), - items: const [ - DropdownMenuItem(value: 0, child: Text('Fully Managed')), - DropdownMenuItem(value: 1, child: Text('Self-Managed')), + items: [ + DropdownMenuItem( + value: 0, + child: Text('siteModeFullyManaged'.tr()), + ), + DropdownMenuItem(value: 1, child: Text('siteModeSelfManaged'.tr())), ], onChanged: (value) { if (value != null) { @@ -104,7 +107,7 @@ class SiteForm extends HookConsumerWidget { ).padding(all: 20); return SheetScaffold( - titleText: 'Edit Publication Site', + titleText: 'editPublicationSite'.tr(), child: Builder( builder: (context) => SingleChildScrollView( @@ -116,7 +119,7 @@ class SiteForm extends HookConsumerWidget { TextButton.icon( onPressed: deleteSite, icon: const Icon(Symbols.delete_forever), - label: const Text('Delete Publication Site'), + label: Text('deletePublicationSite'.tr()), style: TextButton.styleFrom( foregroundColor: Colors.red, ), @@ -171,7 +174,7 @@ class SiteForm extends HookConsumerWidget { ref.invalidate(siteListNotifierProvider(pubName)); if (context.mounted) { - showSnackBar('Publication site saved successfully'); + showSnackBar('publicationSiteSavedSuccess'.tr()); Navigator.pop(context); } } catch (e) { @@ -185,8 +188,8 @@ class SiteForm extends HookConsumerWidget { if (siteSlug == null) return; // Shouldn't happen for editing final confirmed = await showConfirmAlert( - 'Are you sure you want to delete this publication site? This action cannot be undone.', - 'Delete Publication Site', + 'publicationSiteDeleteConfirm'.tr(), + 'deletePublicationSite'.tr(), ); if (confirmed != true) return; @@ -199,7 +202,7 @@ class SiteForm extends HookConsumerWidget { ref.invalidate(siteListNotifierProvider(pubName)); if (context.mounted) { - showSnackBar('Publication site deleted successfully'); + showSnackBar('publicationSiteDeletedSuccess'.tr()); Navigator.pop(context); } } catch (e) { @@ -243,13 +246,13 @@ class SiteForm extends HookConsumerWidget { editingSiteSlug, ), loading: - () => const SheetScaffold( - titleText: 'Edit Publication Site', + () => SheetScaffold( + titleText: 'editPublicationSite'.tr(), child: Center(child: CircularProgressIndicator()), ), error: (error, _) => SheetScaffold( - titleText: 'Edit Publication Site', + titleText: 'editPublicationSite'.tr(), child: ResponseErrorWidget( error: error.toString(), onRetry: () { @@ -327,9 +330,12 @@ class SiteForm extends HookConsumerWidget { borderRadius: BorderRadius.all(Radius.circular(12)), ), ), - items: const [ - DropdownMenuItem(value: 0, child: Text('Fully Managed')), - DropdownMenuItem(value: 1, child: Text('Self-Managed')), + items: [ + DropdownMenuItem( + value: 0, + child: Text('siteModeFullyManaged'.tr()), + ), + DropdownMenuItem(value: 1, child: Text('siteModeSelfManaged'.tr())), ], onChanged: (value) { if (value != null) { @@ -348,7 +354,9 @@ class SiteForm extends HookConsumerWidget { return SheetScaffold( titleText: - siteSlug == null ? 'New Publication Site' : 'Edit Publication Site', + siteSlug == null + ? 'newPublicationSite'.tr() + : 'editPublicationSite'.tr(), child: SingleChildScrollView( child: Column( children: [ @@ -359,7 +367,7 @@ class SiteForm extends HookConsumerWidget { TextButton.icon( onPressed: isLoading.value ? null : deleteSite, icon: const Icon(Symbols.delete_forever), - label: const Text('Delete Publication Site'), + label: Text('deletePublicationSite'.tr()), style: TextButton.styleFrom(foregroundColor: Colors.red), ).alignment(Alignment.centerRight), const SizedBox(height: 16), diff --git a/lib/screens/creators/sites/site_list.dart b/lib/screens/creators/sites/site_list.dart index f55e27df..ca9b44de 100644 --- a/lib/screens/creators/sites/site_list.dart +++ b/lib/screens/creators/sites/site_list.dart @@ -74,7 +74,7 @@ class CreatorSiteListScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return AppScaffold( isNoBackground: false, - appBar: AppBar(title: Text('Publication Sites')), + appBar: AppBar(title: Text('publicationSites'.tr())), floatingActionButton: FloatingActionButton( onPressed: () => _createSite(context), child: Icon(Icons.add), @@ -201,21 +201,19 @@ class _CreatorSiteItem extends HookConsumerWidget { context: context, builder: (context) => AlertDialog( - title: Text('Delete Site'), - content: Text( - 'Are you sure you want to delete this site?', - ), + title: Text('deleteSite'.tr()), + content: Text('deleteSiteConfirm'.tr()), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: Text('Cancel'), + child: Text('cancel'.tr()), ), TextButton( onPressed: () => 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); await client.delete('/zone/sites/${site.id}'); ref.invalidate(siteListNotifierProvider(pubName)); - showSnackBar('Site deleted successfully'); + showSnackBar('siteDeletedSuccess'.tr()); } catch (e) { showErrorAlert(e); } diff --git a/lib/screens/lottery.dart b/lib/screens/lottery.dart index 1f6a8a30..bc3f4f3c 100644 --- a/lib/screens/lottery.dart +++ b/lib/screens/lottery.dart @@ -534,7 +534,7 @@ class _LotteryPurchaseSheetState extends State { ), const Gap(4), Text( - 'The last selected number will be your special number.', + 'lotteryLastNumberSpecial'.tr(), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.w500, @@ -738,11 +738,11 @@ class _LotteryPurchaseSheetState extends State { }, validator: (value) { if (value == null || value.isEmpty) { - return 'Please enter a multiplier'; + return 'lotteryMultiplierRequired'.tr(); } final parsed = int.tryParse(value); if (parsed == null || parsed < 1 || parsed > 10) { - return 'Multiplier must be between 1 and 10'; + return 'lotteryMultiplierRange'.tr(); } return null; }, diff --git a/lib/widgets/sites/file_management_action_section.dart b/lib/widgets/sites/file_management_action_section.dart index 143e3b3b..f6481d57 100644 --- a/lib/widgets/sites/file_management_action_section.dart +++ b/lib/widgets/sites/file_management_action_section.dart @@ -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()])); } } } diff --git a/lib/widgets/sites/file_management_section.dart b/lib/widgets/sites/file_management_section.dart index 55b3f01e..0f5aa02a 100644 --- a/lib/widgets/sites/file_management_section.dart +++ b/lib/widgets/sites/file_management_section.dart @@ -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( + PopupMenuItem( value: 'files', child: Row( children: [ Icon(Symbols.file_copy), Gap(12), - Text('Files'), + Text('siteFiles'.tr()), ], ), ), - const PopupMenuItem( + PopupMenuItem( 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()), ), ], ), diff --git a/lib/widgets/sites/pages_section.dart b/lib/widgets/sites/pages_section.dart index 4163173e..66caf192 100644 --- a/lib/widgets/sites/pages_section.dart +++ b/lib/widgets/sites/pages_section.dart @@ -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()), ), ], ), diff --git a/lib/widgets/sites/site_action_menu.dart b/lib/widgets/sites/site_action_menu.dart index ce44c04e..5df9690d 100644 --- a/lib/widgets/sites/site_action_menu.dart +++ b/lib/widgets/sites/site_action_menu.dart @@ -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) { diff --git a/lib/widgets/sites/site_detail_content.dart b/lib/widgets/sites/site_detail_content.dart index 3fd6219d..2362aa92 100644 --- a/lib/widgets/sites/site_detail_content.dart +++ b/lib/widgets/sites/site_detail_content.dart @@ -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, ), diff --git a/lib/widgets/wallet/fund_envelope.dart b/lib/widgets/wallet/fund_envelope.dart index 93f67a22..f18219b9 100644 --- a/lib/widgets/wallet/fund_envelope.dart +++ b/lib/widgets/wallet/fund_envelope.dart @@ -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, ), ),