num2words_plus 1.0.1 copy "num2words_plus: ^1.0.1" to clipboard
num2words_plus: ^1.0.1 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
7
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