num2words_plus 1.0.0 copy "num2words_plus: ^1.0.0" to clipboard
num2words_plus: ^1.0.0 copied to clipboard

Convert numbers to words for accounting, invoicing, and banking apps. Supports Nepali and English with the Nepali numbering system. Offline-first, zero-dependency, Flutter and Dart compatible.

example/lib/main.dart

// Copyright 2026 Avoloft Technologies
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:num2words_plus/num2words_plus.dart';

void main() {
  runApp(const Num2WordsDemoApp());
}

class Num2WordsDemoApp extends StatelessWidget {
  const Num2WordsDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    final colorScheme = ColorScheme.fromSeed(
      seedColor: const Color(0xFF0F766E),
      brightness: Brightness.light,
    );

    return MaterialApp(
      title: 'num2words_plus demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: colorScheme,
        useMaterial3: true,
        scaffoldBackgroundColor: const Color(0xFFF8FAF7),
        inputDecorationTheme: const InputDecorationTheme(
          border: OutlineInputBorder(),
          filled: true,
          fillColor: Colors.white,
        ),
      ),
      home: const DemoHomePage(),
    );
  }
}

class DemoHomePage extends StatefulWidget {
  const DemoHomePage({super.key});

  @override
  State<DemoHomePage> createState() => _DemoHomePageState();
}

class _DemoHomePageState extends State<DemoHomePage> {
  final _numberController = TextEditingController(text: '1234.56');
  final _primaryUnitController = TextEditingController(text: 'रुपैयाँ');
  final _secondaryUnitController = TextEditingController(text: 'पैसा');

  Num2WordsLanguage _language = Num2WordsLanguage.nepali;
  Num2WordsMode _mode = Num2WordsMode.standard;
  Num2WordsLettercase _letterCase = Num2WordsLettercase.lowercase;
  bool _useCurrencyUnits = false;

  @override
  void dispose() {
    _numberController.dispose();
    _primaryUnitController.dispose();
    _secondaryUnitController.dispose();
    super.dispose();
  }

  bool get _canUseCurrencyUnits =>
      _mode == Num2WordsMode.standard || _mode == Num2WordsMode.check;

  String get _convertedText {
    try {
      final converter = Num2WordsPlus(
        language: _language,
        mode: _mode,
        letterCase: _letterCase,
        primaryUnit: _useCurrencyUnits && _canUseCurrencyUnits
            ? _primaryUnitController.text
            : null,
        secondaryUnit: _useCurrencyUnits && _canUseCurrencyUnits
            ? _secondaryUnitController.text
            : null,
      );

      return converter.parse(_numberController.text);
    } on Num2WordsException catch (error) {
      return error.message;
    } on Object catch (error) {
      return error.toString();
    }
  }

  bool get _hasError {
    try {
      final converter = Num2WordsPlus(
        language: _language,
        mode: _mode,
        letterCase: _letterCase,
        primaryUnit: _useCurrencyUnits && _canUseCurrencyUnits
            ? _primaryUnitController.text
            : null,
        secondaryUnit: _useCurrencyUnits && _canUseCurrencyUnits
            ? _secondaryUnitController.text
            : null,
      );
      converter.parse(_numberController.text);
      return false;
    } on Object {
      return true;
    }
  }

  void _setMode(Num2WordsMode mode) {
    setState(() {
      _mode = mode;

      if (!_canUseCurrencyUnits) {
        _useCurrencyUnits = false;
      }

      if (_mode == Num2WordsMode.check) {
        _language = Num2WordsLanguage.english;
        _letterCase = Num2WordsLettercase.titlecase;
        _numberController.text = '1234.56';
      } else if (_mode == Num2WordsMode.phone) {
        _numberController.text = '9841234567';
      } else if (_mode == Num2WordsMode.year) {
        _numberController.text =
            _language == Num2WordsLanguage.nepali ? '2077' : '2024';
      } else if (_mode == Num2WordsMode.ordinal) {
        _numberController.text = '21';
      }
    });
  }

  void _loadSample(String value) {
    setState(() => _numberController.text = value);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final output = _convertedText;
    final hasError = _hasError;

    return Scaffold(
      body: SafeArea(
        child: CustomScrollView(
          slivers: [
            SliverToBoxAdapter(
              child: _Header(
                onSampleSelected: _loadSample,
                selectedLanguage: _language,
              ),
            ),
            SliverPadding(
              padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
              sliver: SliverToBoxAdapter(
                child: Center(
                  child: ConstrainedBox(
                    constraints: const BoxConstraints(maxWidth: 1120),
                    child: LayoutBuilder(
                      builder: (context, constraints) {
                        final wide = constraints.maxWidth >= 860;
                        final controls = _ControlsPanel(
                          numberController: _numberController,
                          primaryUnitController: _primaryUnitController,
                          secondaryUnitController: _secondaryUnitController,
                          language: _language,
                          mode: _mode,
                          letterCase: _letterCase,
                          useCurrencyUnits: _useCurrencyUnits,
                          canUseCurrencyUnits: _canUseCurrencyUnits,
                          onChanged: () => setState(() {}),
                          onLanguageChanged: (value) {
                            setState(() => _language = value);
                          },
                          onModeChanged: _setMode,
                          onLetterCaseChanged: (value) {
                            setState(() => _letterCase = value);
                          },
                          onCurrencyUnitsChanged: (value) {
                            setState(() => _useCurrencyUnits = value);
                          },
                        );
                        final result = _ResultPanel(
                          output: output,
                          hasError: hasError,
                          onCopy: output.isEmpty
                              ? null
                              : () => Clipboard.setData(
                                    ClipboardData(text: output),
                                  ),
                        );

                        if (wide) {
                          return Row(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Expanded(flex: 5, child: controls),
                              const SizedBox(width: 16),
                              Expanded(flex: 4, child: result),
                            ],
                          );
                        }

                        return Column(
                          children: [
                            controls,
                            const SizedBox(height: 16),
                            result,
                          ],
                        );
                      },
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
      bottomNavigationBar: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border(top: BorderSide(color: theme.dividerColor)),
        ),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
        child: Text(
          'Runs locally in the browser with no network calls.',
          textAlign: TextAlign.center,
          style: theme.textTheme.bodySmall,
        ),
      ),
    );
  }
}

class _Header extends StatelessWidget {
  const _Header({
    required this.onSampleSelected,
    required this.selectedLanguage,
  });

  final ValueChanged<String> onSampleSelected;
  final Num2WordsLanguage selectedLanguage;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Container(
      width: double.infinity,
      decoration: const BoxDecoration(
        color: Color(0xFF17324D),
      ),
      padding: const EdgeInsets.fromLTRB(16, 28, 16, 24),
      child: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 1120),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'num2words_plus',
                style: theme.textTheme.displaySmall?.copyWith(
                  color: Colors.white,
                  fontWeight: FontWeight.w800,
                  letterSpacing: 0,
                ),
              ),
              const SizedBox(height: 8),
              Text(
                selectedLanguage == Num2WordsLanguage.nepali
                    ? 'नेपाली र English number words for invoices, receipts, and forms.'
                    : 'Nepali and English number words for invoices, receipts, and forms.',
                style: theme.textTheme.titleMedium?.copyWith(
                  color: const Color(0xFFE9F3EF),
                ),
              ),
              const SizedBox(height: 18),
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: [
                  _SampleButton(
                    label: '1234.56',
                    onPressed: () => onSampleSelected('1234.56'),
                  ),
                  _SampleButton(
                    label: '100000',
                    onPressed: () => onSampleSelected('100000'),
                  ),
                  _SampleButton(
                    label: '9841234567',
                    onPressed: () => onSampleSelected('9841234567'),
                  ),
                  _SampleButton(
                    label: '2077',
                    onPressed: () => onSampleSelected('2077'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _SampleButton extends StatelessWidget {
  const _SampleButton({
    required this.label,
    required this.onPressed,
  });

  final String label;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return FilledButton.tonalIcon(
      onPressed: onPressed,
      icon: const Icon(Icons.auto_awesome, size: 18),
      label: Text(label),
    );
  }
}

class _ControlsPanel extends StatelessWidget {
  const _ControlsPanel({
    required this.numberController,
    required this.primaryUnitController,
    required this.secondaryUnitController,
    required this.language,
    required this.mode,
    required this.letterCase,
    required this.useCurrencyUnits,
    required this.canUseCurrencyUnits,
    required this.onChanged,
    required this.onLanguageChanged,
    required this.onModeChanged,
    required this.onLetterCaseChanged,
    required this.onCurrencyUnitsChanged,
  });

  final TextEditingController numberController;
  final TextEditingController primaryUnitController;
  final TextEditingController secondaryUnitController;
  final Num2WordsLanguage language;
  final Num2WordsMode mode;
  final Num2WordsLettercase letterCase;
  final bool useCurrencyUnits;
  final bool canUseCurrencyUnits;
  final VoidCallback onChanged;
  final ValueChanged<Num2WordsLanguage> onLanguageChanged;
  final ValueChanged<Num2WordsMode> onModeChanged;
  final ValueChanged<Num2WordsLettercase> onLetterCaseChanged;
  final ValueChanged<bool> onCurrencyUnitsChanged;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
        side: BorderSide(color: theme.dividerColor),
      ),
      child: Padding(
        padding: const EdgeInsets.all(18),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Try a value', style: theme.textTheme.titleLarge),
            const SizedBox(height: 14),
            TextField(
              controller: numberController,
              keyboardType: const TextInputType.numberWithOptions(
                decimal: true,
              ),
              decoration: const InputDecoration(
                labelText: 'Number',
                prefixIcon: Icon(Icons.pin_outlined),
              ),
              onChanged: (_) => onChanged(),
            ),
            const SizedBox(height: 18),
            const _ControlLabel('Language'),
            const SizedBox(height: 8),
            SegmentedButton<Num2WordsLanguage>(
              segments: const [
                ButtonSegment(
                  value: Num2WordsLanguage.nepali,
                  label: Text('Nepali'),
                  icon: Icon(Icons.translate),
                ),
                ButtonSegment(
                  value: Num2WordsLanguage.english,
                  label: Text('English'),
                  icon: Icon(Icons.language),
                ),
              ],
              selected: {language},
              onSelectionChanged: (selection) {
                onLanguageChanged(selection.first);
              },
            ),
            const SizedBox(height: 18),
            const _ControlLabel('Mode'),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: Num2WordsMode.values.map((value) {
                final selected = value == mode;
                return ChoiceChip(
                  label: Text(_modeLabel(value)),
                  avatar: Icon(
                    _modeIcon(value),
                    size: 18,
                    color: selected
                        ? theme.colorScheme.onSecondaryContainer
                        : theme.colorScheme.onSurfaceVariant,
                  ),
                  selected: selected,
                  onSelected: (_) => onModeChanged(value),
                );
              }).toList(),
            ),
            const SizedBox(height: 18),
            const _ControlLabel('Letter case'),
            const SizedBox(height: 8),
            DropdownButtonFormField<Num2WordsLettercase>(
              initialValue: letterCase,
              decoration: const InputDecoration(
                prefixIcon: Icon(Icons.text_fields),
              ),
              items: Num2WordsLettercase.values.map((value) {
                return DropdownMenuItem(
                  value: value,
                  child: Text(_letterCaseLabel(value)),
                );
              }).toList(),
              onChanged: (value) {
                if (value != null) {
                  onLetterCaseChanged(value);
                }
              },
            ),
            const SizedBox(height: 10),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Use currency units'),
              value: useCurrencyUnits && canUseCurrencyUnits,
              onChanged: canUseCurrencyUnits ? onCurrencyUnitsChanged : null,
            ),
            AnimatedSwitcher(
              duration: const Duration(milliseconds: 180),
              child: useCurrencyUnits && canUseCurrencyUnits
                  ? Column(
                      key: const ValueKey('currency-fields'),
                      children: [
                        const SizedBox(height: 6),
                        TextField(
                          controller: primaryUnitController,
                          decoration: const InputDecoration(
                            labelText: 'Primary unit',
                            prefixIcon: Icon(Icons.account_balance_wallet),
                          ),
                          onChanged: (_) => onChanged(),
                        ),
                        const SizedBox(height: 12),
                        TextField(
                          controller: secondaryUnitController,
                          decoration: const InputDecoration(
                            labelText: 'Secondary unit',
                            prefixIcon: Icon(Icons.payments_outlined),
                          ),
                          onChanged: (_) => onChanged(),
                        ),
                      ],
                    )
                  : const SizedBox.shrink(),
            ),
          ],
        ),
      ),
    );
  }

  String _modeLabel(Num2WordsMode value) {
    switch (value) {
      case Num2WordsMode.standard:
        return 'Standard';
      case Num2WordsMode.ordinal:
        return 'Ordinal';
      case Num2WordsMode.year:
        return 'Year';
      case Num2WordsMode.phone:
        return 'Phone';
      case Num2WordsMode.check:
        return 'Check';
    }
  }

  IconData _modeIcon(Num2WordsMode value) {
    switch (value) {
      case Num2WordsMode.standard:
        return Icons.format_list_numbered;
      case Num2WordsMode.ordinal:
        return Icons.emoji_events_outlined;
      case Num2WordsMode.year:
        return Icons.calendar_month_outlined;
      case Num2WordsMode.phone:
        return Icons.phone_iphone;
      case Num2WordsMode.check:
        return Icons.receipt_long_outlined;
    }
  }

  String _letterCaseLabel(Num2WordsLettercase value) {
    switch (value) {
      case Num2WordsLettercase.lowercase:
        return 'lowercase';
      case Num2WordsLettercase.uppercase:
        return 'UPPERCASE';
      case Num2WordsLettercase.titlecase:
        return 'Title Case';
      case Num2WordsLettercase.sentencecase:
        return 'Sentence case';
    }
  }
}

class _ControlLabel extends StatelessWidget {
  const _ControlLabel(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: Theme.of(context).textTheme.labelLarge,
    );
  }
}

class _ResultPanel extends StatelessWidget {
  const _ResultPanel({
    required this.output,
    required this.hasError,
    required this.onCopy,
  });

  final String output;
  final bool hasError;
  final VoidCallback? onCopy;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final color = hasError ? theme.colorScheme.error : const Color(0xFF0F766E);

    return Card(
      elevation: 0,
      color: Colors.white,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
        side: BorderSide(color: color.withValues(alpha: 0.35)),
      ),
      child: Padding(
        padding: const EdgeInsets.all(18),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(
                  hasError ? Icons.error_outline : Icons.check_circle_outline,
                  color: color,
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    hasError ? 'Needs a valid input' : 'Converted words',
                    style: theme.textTheme.titleLarge,
                  ),
                ),
                IconButton.filledTonal(
                  tooltip: 'Copy result',
                  onPressed: onCopy,
                  icon: const Icon(Icons.copy),
                ),
              ],
            ),
            const SizedBox(height: 18),
            SelectableText(
              output,
              style: theme.textTheme.headlineSmall?.copyWith(
                color: hasError ? theme.colorScheme.error : Colors.black87,
                height: 1.35,
                letterSpacing: 0,
              ),
            ),
            const SizedBox(height: 22),
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(14),
              decoration: BoxDecoration(
                color: const Color(0xFFFFF8E7),
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: const Color(0xFFE9C46A)),
              ),
              child: Text(
                'final words = Num2WordsPlus(...).parse(input);',
                style: theme.textTheme.bodyMedium?.copyWith(
                  fontFamily: 'monospace',
                  color: const Color(0xFF4D3A12),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
150
points
115
downloads

Documentation

API reference

Publisher

verified publisheravoloft.com

Weekly Downloads

Convert numbers to words for accounting, invoicing, and banking apps. Supports Nepali and English with the Nepali numbering system. Offline-first, zero-dependency, Flutter and Dart compatible.

Repository (GitHub)
View/report issues
Contributing

Topics

#number #words #nepali #english #currency

License

BSD-3-Clause (license)

More

Packages that depend on num2words_plus