usama bhatti
·Flutter Flow Specialist | CTO @synnestra.co

Unicode Asset Resolver for FlutterFlow

Working with multilingual asset files in Flutter can become surprisingly painful when filenames contain Unicode characters.

Files like:

  • Malmö_2026.json

  • Ålesund_2026.json

  • İstanbul_2026.json

may physically exist inside your assets folder, yet Flutter fails to load them correctly using:

rootBundle.loadString(path)

The reason is Unicode normalization.

Different operating systems store Unicode filenames differently:

  • NFC (composed form)

  • NFD (decomposed form)

macOS commonly stores filenames in decomposed form, while Flutter often expects exact matching. This creates silent asset-loading failures that are extremely difficult to debug — especially inside FlutterFlow custom actions.

After dealing with hundreds of international JSON files for a prayer time application, I built a generic Unicode Asset Resolver that automatically handles these filename inconsistencies.

You simply pass the filename.

The resolver handles the rest.

Features

✅ Unicode-safe asset loading
✅ NFC ↔ NFD normalization support
✅ FlutterFlow compatible
✅ Automatic AssetManifest scanning
✅ Smart fallback matching
✅ Cached asset lookup for performance
✅ Works with JSON, TXT, CSV, and other assets
✅ Cross-platform compatibility

Unicode Asset Resolver

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

class UnicodeAssetResolver {
  UnicodeAssetResolver._();

  static List<String>? _cachedAssets;

  /// Convert common Unicode characters to decomposed form (NFD)
  static String toNfd(String input) {
    const decomposed = <String, String>{
      'å': 'a\u030A',
      'ä': 'a\u0308',
      'ö': 'o\u0308',
      'é': 'e\u0301',
      'è': 'e\u0300',
      'ü': 'u\u0308',
      'Å': 'A\u030A',
      'Ä': 'A\u0308',
      'Ö': 'O\u0308',
      'É': 'E\u0301',
      'İ': 'I\u0307',
    };

    var result = input;

    decomposed.forEach((composed, decomposed) {
      result = result.replaceAll(composed, decomposed);
    });

    return result;
  }

  static bool _fileNamesMatch(String a, String b) {
    final left = a.toLowerCase();
    final right = b.toLowerCase();

    if (left == right) return true;
    if (toNfd(left) == right) return true;
    if (left == toNfd(right)) return true;
    if (toNfd(left) == toNfd(right)) return true;

    return false;
  }

  static Future<List<String>> _allAssets() async {
    if (_cachedAssets != null) {
      return _cachedAssets!;
    }

    final manifest =
        await AssetManifest.loadFromAssetBundle(rootBundle);

    _cachedAssets = manifest.listAssets();

    return _cachedAssets!;
  }

  /// Resolve any asset path using only the filename
  static Future<String?> resolvePath({
    required String fileName,
    String assetRoot = 'assets',
  }) async {
    final normalizedFile = fileName.trim();
    final normalizedFileNfd = toNfd(normalizedFile);

    final assets = await _allAssets();

    /// Direct path attempts
    final directPaths = <String>[
      '$assetRoot/$normalizedFile',
      '$assetRoot/$normalizedFileNfd',
    ];

    for (final path in directPaths) {
      if (assets.contains(path)) {
        return path;
      }
    }

    /// Deep fallback search
    for (final asset in assets) {
      if (!asset.startsWith(assetRoot)) continue;

      final base = asset.split('/').last;

      if (_fileNamesMatch(base, normalizedFile)) {
        return asset;
      }
    }

    return null;
  }

  /// Load any asset as string
  static Future<String> loadString({
    required String fileName,
    String assetRoot = 'assets',
  }) async {
    final path = await resolvePath(
      fileName: fileName,
      assetRoot: assetRoot,
    );

    if (path == null) {
      throw FlutterError(
        'Unicode asset not found: $fileName',
      );
    }

    return rootBundle.loadString(path);
  }

  /// Clear cached asset manifest
  static void clearCache() {
    _cachedAssets = null;
  }
}

Usage

Basic Example

final json = await UnicodeAssetResolver.loadString(
  fileName: 'Malmö_2026.json',
  assetRoot: 'assets/jsons',
);

FlutterFlow Custom Action Example

final data = await UnicodeAssetResolver.loadString(
  fileName: '${cityName}_2026.json',
  assetRoot: 'assets/jsons/2026',
);

Why This Utility Matters

This utility was built from a real-world production problem involving thousands of multilingual JSON files.

Instead of:

  • renaming files manually

  • removing Unicode characters

  • creating duplicate datasets

  • hardcoding paths

this resolver allows Flutter applications to work naturally with international filenames.

Perfect for:

  • Prayer time applications

  • Localization systems

  • Offline datasets

  • Multilingual apps

  • International city databases

  • Translation packages

6
2 replies