♻️ Extract the poll related words

This commit is contained in:
2025-08-13 00:35:44 +08:00
parent 7a31663310
commit 0f481aff5b
4 changed files with 99 additions and 60 deletions

View File

@@ -799,5 +799,42 @@
"filesListAdditional": { "filesListAdditional": {
"one": "+{} file remaining", "one": "+{} file remaining",
"other": "+{} files remaining" "other": "+{} files remaining"
} },
} "pollAnswerSubmitted": "Poll answer has been submitted.",
"modifyAnswers": "Modify Answers",
"back": "Back",
"submit": "Submit",
"pollOptionDefaultLabel": "Option 1",
"pollUpdated": "Poll updated.",
"pollCreated": "Poll created.",
"pollCreate": "Create Poll",
"pollEdit": "Edit Poll",
"pollPreviewJsonDebug": "Debug Preview",
"pollTitleRequired": "Title is required",
"pollEndDateOptional": "End date & time (optional)",
"notSet": "Not set",
"pick": "Pick",
"clear": "Clear",
"questions": "Questions",
"pollAddQuestion": "Add question",
"pollQuestionTypeSingleChoice": "Single choice",
"pollQuestionTypeMultipleChoice": "Multiple choice",
"pollQuestionTypeFreeText": "Free text",
"pollQuestionTypeYesNo": "Yes / No",
"pollQuestionTypeRating": "Rating",
"pollNoQuestionsYet": "No questions yet",
"pollNoQuestionsHint": "Use \"Add question\" to start building your poll.",
"pollDebugPreview": "Debug Preview",
"pollUntitledQuestion": "Untitled question",
"moveUp": "Move up",
"moveDown": "Move down",
"required": "Required",
"pollQuestionTitle": "Question title",
"pollQuestionTitleRequired": "Question title is required",
"pollQuestionDescriptionOptional": "Question description (optional)",
"options": "Options",
"pollAddOption": "Add option",
"pollOptionLabel": "Option label",
"pollLongTextAnswerPreview": "Long text answer (preview)",
"pollShortTextAnswerPreview": "Short text answer (preview)"
}

View File

@@ -1,5 +1,5 @@
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';

View File

@@ -11,6 +11,7 @@ import 'package:island/widgets/alert.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:easy_localization/easy_localization.dart';
class PollEditorState { class PollEditorState {
String? id; // for editing String? id; // for editing
@@ -110,7 +111,7 @@ class PollEditor extends Notifier<PollEditorState> {
? [ ? [
SnPollOption( SnPollOption(
id: const Uuid().v4(), id: const Uuid().v4(),
label: 'Option 1', label: 'pollOptionDefaultLabel'.tr(),
order: 0, order: 0,
), ),
] ]
@@ -191,7 +192,7 @@ class PollEditor extends Notifier<PollEditorState> {
: [ : [
SnPollOption( SnPollOption(
id: const Uuid().v4(), id: const Uuid().v4(),
label: 'Option 1', label: 'pollOptionDefaultLabel'.tr(),
order: 0, order: 0,
), ),
]) ])
@@ -389,7 +390,7 @@ class PollEditorScreen extends ConsumerWidget {
data: body, data: body,
)); ));
showSnackBar(isUpdate ? 'Poll updated.' : 'Poll created.'); showSnackBar(isUpdate ? 'pollUpdated'.tr() : 'pollCreated'.tr());
if (!context.mounted) return; if (!context.mounted) return;
Navigator.of(context).maybePop(res.data); Navigator.of(context).maybePop(res.data);
@@ -416,11 +417,11 @@ class PollEditorScreen extends ConsumerWidget {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'), title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
actions: [ actions: [
if (kDebugMode) if (kDebugMode)
IconButton( IconButton(
tooltip: 'Preview JSON (debug)', tooltip: 'pollPreviewJsonDebug'.tr(),
onPressed: () { onPressed: () {
_showDebugPreview(context, model); _showDebugPreview(context, model);
}, },
@@ -439,8 +440,8 @@ class PollEditorScreen extends ConsumerWidget {
children: [ children: [
TextFormField( TextFormField(
initialValue: model.title ?? '', initialValue: model.title ?? '',
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Title', labelText: 'title'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -452,7 +453,7 @@ class PollEditorScreen extends ConsumerWidget {
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
validator: (v) { validator: (v) {
if (v == null || v.trim().isEmpty) { if (v == null || v.trim().isEmpty) {
return 'Title is required'; return 'pollTitleRequired'.tr();
} }
return null; return null;
}, },
@@ -460,8 +461,8 @@ class PollEditorScreen extends ConsumerWidget {
const Gap(12), const Gap(12),
TextFormField( TextFormField(
initialValue: model.description ?? '', initialValue: model.description ?? '',
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Description', labelText: 'description'.tr(),
alignLabelWithHint: true, alignLabelWithHint: true,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
@@ -482,7 +483,7 @@ class PollEditorScreen extends ConsumerWidget {
Row( Row(
children: [ children: [
Text( Text(
'Questions', 'questions'.tr(),
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
const Spacer(), const Spacer(),
@@ -495,7 +496,7 @@ class PollEditorScreen extends ConsumerWidget {
: controller.open(); : controller.open();
}, },
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text('Add question'), label: Text('pollAddQuestion'.tr()),
); );
}, },
menuChildren: menuChildren:
@@ -514,9 +515,9 @@ class PollEditorScreen extends ConsumerWidget {
const Gap(8), const Gap(8),
if (model.questions.isEmpty) if (model.questions.isEmpty)
_EmptyState( _EmptyState(
title: 'No questions yet', title: 'pollNoQuestionsYet'.tr(),
subtitle: subtitle:
'Use "Add question" to start building your poll.', 'pollNoQuestionsHint'.tr(),
) )
else else
ReorderableListView.builder( ReorderableListView.builder(
@@ -585,7 +586,7 @@ class PollEditorScreen extends ConsumerWidget {
Navigator.of(context).maybePop(); Navigator.of(context).maybePop();
}, },
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
label: const Text('Cancel'), label: Text('cancel'.tr()),
), ),
const Spacer(), const Spacer(),
FilledButton.icon( FilledButton.icon(
@@ -593,7 +594,7 @@ class PollEditorScreen extends ConsumerWidget {
_submitPoll(context, ref); _submitPoll(context, ref);
}, },
icon: const Icon(Icons.cloud_upload_outlined), icon: const Icon(Icons.cloud_upload_outlined),
label: Text(model.id == null ? 'Create' : 'Update'), label: Text(model.id == null ? 'create'.tr() : 'update'.tr()),
), ),
], ],
), ),
@@ -637,14 +638,14 @@ class PollEditorScreen extends ConsumerWidget {
context: context, context: context,
builder: builder:
(_) => AlertDialog( (_) => AlertDialog(
title: const Text('Debug Preview'), title: Text('pollDebugPreview'.tr()),
content: SingleChildScrollView( content: SingleChildScrollView(
child: SelectableText(buf.toString()), child: SelectableText(buf.toString()),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'), child: Text('close'.tr()),
), ),
], ],
), ),
@@ -673,15 +674,15 @@ IconData _iconForType(SnPollQuestionType t) {
String _labelForType(SnPollQuestionType t) { String _labelForType(SnPollQuestionType t) {
switch (t) { switch (t) {
case SnPollQuestionType.singleChoice: case SnPollQuestionType.singleChoice:
return 'Single choice'; return 'pollQuestionTypeSingleChoice'.tr();
case SnPollQuestionType.multipleChoice: case SnPollQuestionType.multipleChoice:
return 'Multiple choice'; return 'pollQuestionTypeMultipleChoice'.tr();
case SnPollQuestionType.freeText: case SnPollQuestionType.freeText:
return 'Free text'; return 'pollQuestionTypeFreeText'.tr();
case SnPollQuestionType.yesNo: case SnPollQuestionType.yesNo:
return 'Yes / No'; return 'pollQuestionTypeYesNo'.tr();
case SnPollQuestionType.rating: case SnPollQuestionType.rating:
return 'Rating'; return 'pollQuestionTypeRating'.tr();
} }
} }
@@ -698,8 +699,8 @@ class _EndDatePicker extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: InputDecorator( child: InputDecorator(
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'End date & time (optional)', labelText: 'pollEndDateOptional'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -711,7 +712,7 @@ class _EndDatePicker extends StatelessWidget {
Icon(Icons.event, color: Theme.of(context).colorScheme.primary), Icon(Icons.event, color: Theme.of(context).colorScheme.primary),
Text( Text(
value == null value == null
? 'Not set' ? 'notSet'.tr()
: MaterialLocalizations.of( : MaterialLocalizations.of(
context, context,
).formatFullDate(value!), ).formatFullDate(value!),
@@ -759,12 +760,12 @@ class _EndDatePicker extends StatelessWidget {
); );
onChanged(dt); onChanged(dt);
}, },
child: const Text('Pick'), child: Text('pick'.tr()),
), ),
if (value != null) if (value != null)
TextButton( TextButton(
onPressed: () => onChanged(null), onPressed: () => onChanged(null),
child: const Text('Clear'), child: Text('clear'.tr()),
), ),
], ],
), ),
@@ -799,7 +800,7 @@ class _QuestionHeader extends StatelessWidget {
child: const Icon(Icons.drag_handle), child: const Icon(Icons.drag_handle),
), ),
title: Text( title: Text(
question.title.isEmpty ? 'Untitled question' : question.title, question.title.isEmpty ? 'pollUntitledQuestion'.tr() : question.title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -808,17 +809,17 @@ class _QuestionHeader extends StatelessWidget {
spacing: 4, spacing: 4,
children: [ children: [
IconButton( IconButton(
tooltip: 'Move up', tooltip: 'moveUp'.tr(),
onPressed: onMoveUp, onPressed: onMoveUp,
icon: const Icon(Icons.arrow_upward), icon: const Icon(Icons.arrow_upward),
), ),
IconButton( IconButton(
tooltip: 'Move down', tooltip: 'moveDown'.tr(),
onPressed: onMoveDown, onPressed: onMoveDown,
icon: const Icon(Icons.arrow_downward), icon: const Icon(Icons.arrow_downward),
), ),
IconButton( IconButton(
tooltip: 'Delete', tooltip: 'delete'.tr(),
onPressed: onDelete, onPressed: onDelete,
icon: const Icon(Icons.delete_outline), icon: const Icon(Icons.delete_outline),
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
@@ -853,7 +854,7 @@ class _QuestionEditor extends ConsumerWidget {
onChanged: (t) => notifier.setQuestionType(index, t), onChanged: (t) => notifier.setQuestionType(index, t),
), ),
FilterChip( FilterChip(
label: const Text('Required'), label: Text('required'.tr()),
selected: question.isRequired, selected: question.isRequired,
onSelected: (v) => notifier.setQuestionRequired(index, v), onSelected: (v) => notifier.setQuestionRequired(index, v),
avatar: Icon( avatar: Icon(
@@ -867,8 +868,8 @@ class _QuestionEditor extends ConsumerWidget {
const Gap(12), const Gap(12),
TextFormField( TextFormField(
initialValue: question.title, initialValue: question.title,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Question title', labelText: 'pollQuestionTitle'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -879,7 +880,7 @@ class _QuestionEditor extends ConsumerWidget {
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
validator: (v) { validator: (v) {
if (v == null || v.trim().isEmpty) { if (v == null || v.trim().isEmpty) {
return 'Question title is required'; return 'pollQuestionTitleRequired'.tr();
} }
return null; return null;
}, },
@@ -887,8 +888,8 @@ class _QuestionEditor extends ConsumerWidget {
const Gap(12), const Gap(12),
TextFormField( TextFormField(
initialValue: question.description ?? '', initialValue: question.description ?? '',
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Question description (optional)', labelText: 'pollQuestionDescriptionOptional'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -902,7 +903,7 @@ class _QuestionEditor extends ConsumerWidget {
), ),
if (question.options != null) ...[ if (question.options != null) ...[
const Gap(16), const Gap(16),
Text('Options', style: Theme.of(context).textTheme.titleMedium), Text('options'.tr(), style: Theme.of(context).textTheme.titleMedium),
const Gap(8), const Gap(8),
_OptionsEditor(index: index, options: question.options!), _OptionsEditor(index: index, options: question.options!),
const Gap(4), const Gap(4),
@@ -911,7 +912,7 @@ class _QuestionEditor extends ConsumerWidget {
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () => notifier.addOption(index), onPressed: () => notifier.addOption(index),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text('Add option'), label: Text('pollAddOption'.tr()),
), ),
), ),
], ],
@@ -937,8 +938,8 @@ class _QuestionTypePicker extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DropdownButtonFormField<SnPollQuestionType>( return DropdownButtonFormField<SnPollQuestionType>(
value: value, value: value,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Type', labelText: 'Type'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -987,8 +988,8 @@ class _OptionsEditor extends ConsumerWidget {
child: TextFormField( child: TextFormField(
key: ValueKey(options[i].id), key: ValueKey(options[i].id),
initialValue: options[i].label, initialValue: options[i].label,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Option label', labelText: 'pollOptionLabel'.tr(),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -1003,7 +1004,7 @@ class _OptionsEditor extends ConsumerWidget {
SizedBox( SizedBox(
width: 40, width: 40,
child: IconButton( child: IconButton(
tooltip: 'Move up', tooltip: 'moveUp'.tr(),
onPressed: onPressed:
i > 0 ? () => notifier.moveOptionUp(index, i) : null, i > 0 ? () => notifier.moveOptionUp(index, i) : null,
icon: const Icon(Icons.arrow_upward), icon: const Icon(Icons.arrow_upward),
@@ -1012,7 +1013,7 @@ class _OptionsEditor extends ConsumerWidget {
SizedBox( SizedBox(
width: 40, width: 40,
child: IconButton( child: IconButton(
tooltip: 'Move down', tooltip: 'moveDown'.tr(),
onPressed: onPressed:
i < options.length - 1 i < options.length - 1
? () => notifier.moveOptionDown(index, i) ? () => notifier.moveOptionDown(index, i)
@@ -1023,7 +1024,7 @@ class _OptionsEditor extends ConsumerWidget {
SizedBox( SizedBox(
width: 40, width: 40,
child: IconButton( child: IconButton(
tooltip: 'Delete', tooltip: 'delete'.tr(),
onPressed: () => notifier.removeOption(index, i), onPressed: () => notifier.removeOption(index, i),
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
), ),
@@ -1048,7 +1049,7 @@ class _TextAnswerPreview extends StatelessWidget {
maxLines: long ? 4 : 1, maxLines: long ? 4 : 1,
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText:
long ? 'Long text answer (preview)' : 'Short text answer (preview)', long ? 'pollLongTextAnswerPreview'.tr() : 'pollShortTextAnswerPreview'.tr(),
border: const OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -1082,9 +1083,9 @@ class _EmptyState extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: Theme.of(context).textTheme.titleMedium), Text('pollNoQuestionsYet'.tr(), style: Theme.of(context).textTheme.titleMedium),
const Gap(4), const Gap(4),
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium), Text('pollNoQuestionsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium),
], ],
), ),
), ),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
@@ -212,7 +213,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
// Only call onSubmit after server accepts // Only call onSubmit after server accepts
widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers)); widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers));
showSnackBar('Poll answer has been submitted.'); showSnackBar('pollAnswerSubmitted'.tr());
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
} catch (e) { } catch (e) {
showErrorAlert(e); showErrorAlert(e);
@@ -393,9 +394,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
children: [ children: [
Expanded( Expanded(
child: SegmentedButton<bool>( child: SegmentedButton<bool>(
segments: const [ segments: [
ButtonSegment(value: true, label: Text('Yes')), ButtonSegment(value: true, label: Text('yes'.tr())),
ButtonSegment(value: false, label: Text('No')), ButtonSegment(value: false, label: Text('no'.tr())),
], ],
selected: _yesNoSelected == null ? {} : {_yesNoSelected!}, selected: _yesNoSelected == null ? {} : {_yesNoSelected!},
onSelectionChanged: (sel) { onSelectionChanged: (sel) {
@@ -448,7 +449,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
// If poll is submitted and not in modification mode, show "Modify" button // If poll is submitted and not in modification mode, show "Modify" button
return FilledButton.icon( return FilledButton.icon(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
label: const Text('Modify Answers'), label: Text('modifyAnswers'.tr()),
onPressed: () { onPressed: () {
setState(() { setState(() {
_isModifying = true; _isModifying = true;
@@ -463,7 +464,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
label: Text(_index == 0 ? 'Cancel' : 'Back'), label: Text(_index == 0 ? 'cancel'.tr() : 'back'.tr()),
onPressed: onPressed:
_submitting _submitting
? null ? null
@@ -488,7 +489,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: Icon(isLast ? Icons.check : Icons.arrow_forward), : Icon(isLast ? Icons.check : Icons.arrow_forward),
label: Text(isLast ? 'Submit' : 'Next'), label: Text(isLast ? 'submit'.tr() : 'next'.tr()),
onPressed: canProceed ? _next : null, onPressed: canProceed ? _next : null,
), ),
], ],