← Back to Blog

Flutter Autocomplete Localization: Suggestions, Hints, and No-Results Messages

flutterautocompletesearchlocalizationsuggestionsaccessibility

Flutter Autocomplete Localization: Suggestions, Hints, and No-Results Messages

Autocomplete widgets enhance user experience by providing suggestions as users type. Localizing autocomplete components ensures users worldwide can efficiently find what they're looking for with suggestions, hints, and feedback in their language. This guide covers everything you need to know about localizing autocomplete in Flutter.

Understanding Autocomplete Localization Needs

Autocomplete requires localization for:

  • Hint text: "Search..." or "Type to search..."
  • Suggestions: Localized option labels
  • No results message: "No matches found"
  • Loading states: "Searching..."
  • Error messages: "Failed to load suggestions"
  • Accessibility: Screen reader announcements
  • Character handling: Unicode, diacritics, RTL text

Basic Autocomplete with Localization

Start with a properly localized autocomplete widget:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Autocomplete<String>(
      optionsBuilder: (textEditingValue) {
        if (textEditingValue.text.isEmpty) {
          return const Iterable<String>.empty();
        }
        return _getLocalizedOptions(context).where((option) {
          return option
              .toLowerCase()
              .contains(textEditingValue.text.toLowerCase());
        });
      },
      fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
        return TextField(
          controller: controller,
          focusNode: focusNode,
          decoration: InputDecoration(
            hintText: l10n.autocompleteHint,
            prefixIcon: const Icon(Icons.search),
            border: const OutlineInputBorder(),
          ),
          onSubmitted: (value) => onSubmitted(),
        );
      },
      optionsViewBuilder: (context, onSelected, options) {
        return _buildOptionsView(context, onSelected, options, l10n);
      },
    );
  }

  List<String> _getLocalizedOptions(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    return [
      l10n.countryUnitedStates,
      l10n.countryUnitedKingdom,
      l10n.countryGermany,
      l10n.countryFrance,
      l10n.countryJapan,
      l10n.countryChina,
      l10n.countryBrazil,
      l10n.countryIndia,
    ];
  }

  Widget _buildOptionsView(
    BuildContext context,
    AutocompleteOnSelected<String> onSelected,
    Iterable<String> options,
    AppLocalizations l10n,
  ) {
    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        elevation: 4,
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),
          child: ListView.builder(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            itemCount: options.length,
            itemBuilder: (context, index) {
              final option = options.elementAt(index);
              return ListTile(
                title: Text(option),
                onTap: () => onSelected(option),
              );
            },
          ),
        ),
      ),
    );
  }
}

ARB file entries:

{
  "autocompleteHint": "Search countries...",
  "@autocompleteHint": {
    "description": "Hint text for country autocomplete"
  },
  "countryUnitedStates": "United States",
  "countryUnitedKingdom": "United Kingdom",
  "countryGermany": "Germany",
  "countryFrance": "France",
  "countryJapan": "Japan",
  "countryChina": "China",
  "countryBrazil": "Brazil",
  "countryIndia": "India"
}

Async Autocomplete with Loading States

Handle async data loading with proper loading messages:

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

  @override
  State<AsyncLocalizedAutocomplete> createState() =>
      _AsyncLocalizedAutocompleteState();
}

class _AsyncLocalizedAutocompleteState
    extends State<AsyncLocalizedAutocomplete> {
  bool _isLoading = false;
  String? _error;
  List<String> _cachedResults = [];

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l10n.autocompleteLabel,
          style: Theme.of(context).textTheme.labelLarge,
        ),
        const SizedBox(height: 8),
        Autocomplete<String>(
          optionsBuilder: (textEditingValue) async {
            return await _searchOptions(textEditingValue.text, l10n);
          },
          fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
            return TextField(
              controller: controller,
              focusNode: focusNode,
              decoration: InputDecoration(
                hintText: l10n.autocompleteHint,
                prefixIcon: const Icon(Icons.search),
                suffixIcon: _isLoading
                    ? const SizedBox(
                        width: 20,
                        height: 20,
                        child: Padding(
                          padding: EdgeInsets.all(12),
                          child: CircularProgressIndicator(strokeWidth: 2),
                        ),
                      )
                    : null,
                border: const OutlineInputBorder(),
                errorText: _error,
              ),
              onSubmitted: (value) => onSubmitted(),
            );
          },
          optionsViewBuilder: (context, onSelected, options) {
            if (options.isEmpty && !_isLoading) {
              return _buildNoResultsView(l10n);
            }
            return _buildOptionsView(context, onSelected, options);
          },
        ),
      ],
    );
  }

  Future<Iterable<String>> _searchOptions(
    String query,
    AppLocalizations l10n,
  ) async {
    if (query.isEmpty) {
      return const Iterable<String>.empty();
    }

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      // Simulate API call
      await Future.delayed(const Duration(milliseconds: 500));

      final results = await _fetchFromApi(query);
      setState(() => _isLoading = false);
      return results;
    } catch (e) {
      setState(() {
        _isLoading = false;
        _error = l10n.autocompleteError;
      });
      return const Iterable<String>.empty();
    }
  }

  Future<List<String>> _fetchFromApi(String query) async {
    // Replace with actual API call
    final allOptions = [
      'Apple', 'Banana', 'Cherry', 'Date', 'Elderberry',
      'Fig', 'Grape', 'Honeydew', 'Kiwi', 'Lemon',
    ];

    return allOptions
        .where((o) => o.toLowerCase().contains(query.toLowerCase()))
        .toList();
  }

  Widget _buildNoResultsView(AppLocalizations l10n) {
    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        elevation: 4,
        child: Container(
          width: 300,
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Icon(
                Icons.search_off,
                color: Theme.of(context).hintColor,
              ),
              const SizedBox(width: 12),
              Text(
                l10n.autocompleteNoResults,
                style: TextStyle(color: Theme.of(context).hintColor),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildOptionsView(
    BuildContext context,
    AutocompleteOnSelected<String> onSelected,
    Iterable<String> options,
  ) {
    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        elevation: 4,
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),
          child: ListView.builder(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            itemCount: options.length,
            itemBuilder: (context, index) {
              final option = options.elementAt(index);
              return ListTile(
                title: Text(option),
                onTap: () => onSelected(option),
              );
            },
          ),
        ),
      ),
    );
  }
}

ARB entries:

{
  "autocompleteLabel": "Search",
  "autocompleteHint": "Start typing to search...",
  "autocompleteNoResults": "No results found",
  "autocompleteError": "Failed to load suggestions",
  "autocompleteLoading": "Searching..."
}

Localized Object Autocomplete

Autocomplete with complex objects and localized display:

class Product {
  final String id;
  final String nameKey;
  final String descriptionKey;
  final double price;

  const Product({
    required this.id,
    required this.nameKey,
    required this.descriptionKey,
    required this.price,
  });
}

class ProductAutocomplete extends StatelessWidget {
  final ValueChanged<Product> onProductSelected;

  const ProductAutocomplete({
    super.key,
    required this.onProductSelected,
  });

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final locale = Localizations.localeOf(context);

    return Autocomplete<Product>(
      optionsBuilder: (textEditingValue) {
        if (textEditingValue.text.isEmpty) {
          return const Iterable<Product>.empty();
        }
        return _getProducts().where((product) {
          final localizedName = _getLocalizedName(product, l10n);
          return localizedName
              .toLowerCase()
              .contains(textEditingValue.text.toLowerCase());
        });
      },
      displayStringForOption: (product) => _getLocalizedName(product, l10n),
      fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
        return TextField(
          controller: controller,
          focusNode: focusNode,
          decoration: InputDecoration(
            hintText: l10n.productSearchHint,
            prefixIcon: const Icon(Icons.shopping_bag_outlined),
            border: const OutlineInputBorder(),
          ),
          onSubmitted: (value) => onSubmitted(),
        );
      },
      optionsViewBuilder: (context, onSelected, options) {
        return _buildProductOptions(context, onSelected, options, l10n, locale);
      },
      onSelected: onProductSelected,
    );
  }

  List<Product> _getProducts() {
    return const [
      Product(
        id: '1',
        nameKey: 'productLaptop',
        descriptionKey: 'productLaptopDesc',
        price: 999.99,
      ),
      Product(
        id: '2',
        nameKey: 'productPhone',
        descriptionKey: 'productPhoneDesc',
        price: 699.99,
      ),
      Product(
        id: '3',
        nameKey: 'productTablet',
        descriptionKey: 'productTabletDesc',
        price: 499.99,
      ),
      Product(
        id: '4',
        nameKey: 'productHeadphones',
        descriptionKey: 'productHeadphonesDesc',
        price: 199.99,
      ),
    ];
  }

  String _getLocalizedName(Product product, AppLocalizations l10n) {
    switch (product.nameKey) {
      case 'productLaptop':
        return l10n.productLaptop;
      case 'productPhone':
        return l10n.productPhone;
      case 'productTablet':
        return l10n.productTablet;
      case 'productHeadphones':
        return l10n.productHeadphones;
      default:
        return product.nameKey;
    }
  }

  String _getLocalizedDescription(Product product, AppLocalizations l10n) {
    switch (product.descriptionKey) {
      case 'productLaptopDesc':
        return l10n.productLaptopDesc;
      case 'productPhoneDesc':
        return l10n.productPhoneDesc;
      case 'productTabletDesc':
        return l10n.productTabletDesc;
      case 'productHeadphonesDesc':
        return l10n.productHeadphonesDesc;
      default:
        return product.descriptionKey;
    }
  }

  Widget _buildProductOptions(
    BuildContext context,
    AutocompleteOnSelected<Product> onSelected,
    Iterable<Product> options,
    AppLocalizations l10n,
    Locale locale,
  ) {
    final currencyFormat = NumberFormat.currency(
      locale: locale.toString(),
      symbol: '\$',
    );

    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        elevation: 4,
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxHeight: 300, maxWidth: 400),
          child: ListView.builder(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            itemCount: options.length,
            itemBuilder: (context, index) {
              final product = options.elementAt(index);
              return ListTile(
                leading: const Icon(Icons.inventory_2_outlined),
                title: Text(_getLocalizedName(product, l10n)),
                subtitle: Text(_getLocalizedDescription(product, l10n)),
                trailing: Text(
                  currencyFormat.format(product.price),
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                onTap: () => onSelected(product),
              );
            },
          ),
        ),
      ),
    );
  }
}

Multi-Language Search with Diacritics

Handle searches across languages with special characters:

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Autocomplete<String>(
      optionsBuilder: (textEditingValue) {
        if (textEditingValue.text.isEmpty) {
          return const Iterable<String>.empty();
        }
        return _getCities(l10n).where((city) {
          return _matchesIgnoringDiacritics(
            city,
            textEditingValue.text,
          );
        });
      },
      fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
        return TextField(
          controller: controller,
          focusNode: focusNode,
          decoration: InputDecoration(
            hintText: l10n.citySearchHint,
            prefixIcon: const Icon(Icons.location_city),
            border: const OutlineInputBorder(),
            helperText: l10n.citySearchHelper,
          ),
          onSubmitted: (value) => onSubmitted(),
        );
      },
    );
  }

  List<String> _getCities(AppLocalizations l10n) {
    return [
      'München', // Munich in German
      'Zürich', // Zurich with umlaut
      'Côte d\'Ivoire',
      'São Paulo',
      'Москва', // Moscow in Russian
      '東京', // Tokyo in Japanese
      'القاهرة', // Cairo in Arabic
      'Kraków', // Krakow with accent
    ];
  }

  bool _matchesIgnoringDiacritics(String option, String query) {
    final normalizedOption = _removeDiacritics(option.toLowerCase());
    final normalizedQuery = _removeDiacritics(query.toLowerCase());
    return normalizedOption.contains(normalizedQuery);
  }

  String _removeDiacritics(String text) {
    // Map of diacritics to base characters
    const diacriticsMap = {
      'à': 'a', 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a',
      'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e',
      'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i',
      'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o',
      'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u',
      'ç': 'c', 'ñ': 'n', 'ß': 'ss',
      'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n',
      'ó': 'o', 'ś': 's', 'ź': 'z', 'ż': 'z',
    };

    return text.split('').map((char) {
      return diacriticsMap[char] ?? char;
    }).join();
  }
}

Autocomplete with Categories

Group suggestions by category with localized headers:

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Autocomplete<SearchResult>(
      optionsBuilder: (textEditingValue) {
        if (textEditingValue.text.isEmpty) {
          return const Iterable<SearchResult>.empty();
        }
        return _searchAll(textEditingValue.text, l10n);
      },
      displayStringForOption: (result) => result.title,
      optionsViewBuilder: (context, onSelected, options) {
        return _buildCategorizedOptions(context, onSelected, options, l10n);
      },
      fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
        return TextField(
          controller: controller,
          focusNode: focusNode,
          decoration: InputDecoration(
            hintText: l10n.globalSearchHint,
            prefixIcon: const Icon(Icons.search),
            border: const OutlineInputBorder(),
          ),
          onSubmitted: (value) => onSubmitted(),
        );
      },
    );
  }

  List<SearchResult> _searchAll(String query, AppLocalizations l10n) {
    final results = <SearchResult>[];
    final lowerQuery = query.toLowerCase();

    // Search products
    final products = [
      SearchResult(
        title: l10n.productLaptop,
        category: SearchCategory.products,
        icon: Icons.laptop,
      ),
      SearchResult(
        title: l10n.productPhone,
        category: SearchCategory.products,
        icon: Icons.phone_android,
      ),
    ];

    // Search articles
    final articles = [
      SearchResult(
        title: l10n.articleGettingStarted,
        category: SearchCategory.articles,
        icon: Icons.article,
      ),
      SearchResult(
        title: l10n.articleBestPractices,
        category: SearchCategory.articles,
        icon: Icons.article,
      ),
    ];

    // Search users
    final users = [
      SearchResult(
        title: 'John Doe',
        category: SearchCategory.users,
        icon: Icons.person,
      ),
      SearchResult(
        title: 'Jane Smith',
        category: SearchCategory.users,
        icon: Icons.person,
      ),
    ];

    for (final item in [...products, ...articles, ...users]) {
      if (item.title.toLowerCase().contains(lowerQuery)) {
        results.add(item);
      }
    }

    return results;
  }

  Widget _buildCategorizedOptions(
    BuildContext context,
    AutocompleteOnSelected<SearchResult> onSelected,
    Iterable<SearchResult> options,
    AppLocalizations l10n,
  ) {
    // Group by category
    final grouped = <SearchCategory, List<SearchResult>>{};
    for (final option in options) {
      grouped.putIfAbsent(option.category, () => []).add(option);
    }

    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        elevation: 4,
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxHeight: 400, maxWidth: 350),
          child: ListView(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            children: [
              for (final entry in grouped.entries) ...[
                _buildCategoryHeader(entry.key, l10n, context),
                for (final result in entry.value)
                  ListTile(
                    leading: Icon(result.icon),
                    title: Text(result.title),
                    dense: true,
                    onTap: () => onSelected(result),
                  ),
              ],
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildCategoryHeader(
    SearchCategory category,
    AppLocalizations l10n,
    BuildContext context,
  ) {
    String title;
    switch (category) {
      case SearchCategory.products:
        title = l10n.categoryProducts;
        break;
      case SearchCategory.articles:
        title = l10n.categoryArticles;
        break;
      case SearchCategory.users:
        title = l10n.categoryUsers;
        break;
    }

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      color: Theme.of(context).colorScheme.surfaceVariant,
      child: Text(
        title,
        style: Theme.of(context).textTheme.labelSmall?.copyWith(
              fontWeight: FontWeight.bold,
            ),
      ),
    );
  }
}

enum SearchCategory { products, articles, users }

class SearchResult {
  final String title;
  final SearchCategory category;
  final IconData icon;

  const SearchResult({
    required this.title,
    required this.category,
    required this.icon,
  });
}

ARB entries:

{
  "globalSearchHint": "Search products, articles, users...",
  "categoryProducts": "Products",
  "categoryArticles": "Articles",
  "categoryUsers": "Users",
  "productLaptop": "Laptop Pro",
  "productPhone": "Smartphone Ultra",
  "articleGettingStarted": "Getting Started Guide",
  "articleBestPractices": "Best Practices"
}

Accessibility for Autocomplete

Ensure autocomplete is accessible to all users:

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Semantics(
      label: l10n.autocompleteAccessibilityLabel,
      child: Autocomplete<String>(
        optionsBuilder: (textEditingValue) {
          if (textEditingValue.text.isEmpty) {
            return const Iterable<String>.empty();
          }
          return _getOptions(l10n).where((option) {
            return option
                .toLowerCase()
                .contains(textEditingValue.text.toLowerCase());
          });
        },
        fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
          return TextField(
            controller: controller,
            focusNode: focusNode,
            decoration: InputDecoration(
              hintText: l10n.autocompleteHint,
              prefixIcon: const Icon(Icons.search),
              border: const OutlineInputBorder(),
            ),
            onSubmitted: (value) => onSubmitted(),
          );
        },
        optionsViewBuilder: (context, onSelected, options) {
          return _buildAccessibleOptions(context, onSelected, options, l10n);
        },
      ),
    );
  }

  List<String> _getOptions(AppLocalizations l10n) {
    return [l10n.option1, l10n.option2, l10n.option3];
  }

  Widget _buildAccessibleOptions(
    BuildContext context,
    AutocompleteOnSelected<String> onSelected,
    Iterable<String> options,
    AppLocalizations l10n,
  ) {
    final optionsList = options.toList();

    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        elevation: 4,
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),
          child: ListView.builder(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            itemCount: optionsList.length,
            itemBuilder: (context, index) {
              final option = optionsList[index];
              return Semantics(
                label: l10n.autocompleteOptionAnnouncement(
                  index + 1,
                  optionsList.length,
                  option,
                ),
                child: ListTile(
                  title: Text(option),
                  onTap: () {
                    onSelected(option);
                    // Announce selection
                    SemanticsService.announce(
                      l10n.autocompleteSelectedAnnouncement(option),
                      Directionality.of(context),
                    );
                  },
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

ARB entries:

{
  "autocompleteAccessibilityLabel": "Search with autocomplete suggestions",
  "autocompleteOptionAnnouncement": "Option {index} of {total}: {option}",
  "@autocompleteOptionAnnouncement": {
    "placeholders": {
      "index": {"type": "int"},
      "total": {"type": "int"},
      "option": {"type": "String"}
    }
  },
  "autocompleteSelectedAnnouncement": "Selected {option}",
  "@autocompleteSelectedAnnouncement": {
    "placeholders": {
      "option": {"type": "String"}
    }
  }
}

Testing Autocomplete Localization

Write comprehensive tests for autocomplete behavior:

void main() {
  group('LocalizedAutocomplete Tests', () {
    testWidgets('displays localized hint text', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: const [
            AppLocalizations.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
          ],
          locale: const Locale('en'),
          home: const Scaffold(
            body: LocalizedAutocomplete(),
          ),
        ),
      );

      expect(find.text('Search countries...'), findsOneWidget);
    });

    testWidgets('shows localized no results message', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: const [
            AppLocalizations.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
          ],
          locale: const Locale('en'),
          home: const Scaffold(
            body: AsyncLocalizedAutocomplete(),
          ),
        ),
      );

      // Enter text that won't match
      await tester.enterText(find.byType(TextField), 'xyz123');
      await tester.pumpAndSettle();

      expect(find.text('No results found'), findsOneWidget);
    });

    testWidgets('filters options in current locale', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: const [
            AppLocalizations.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
          ],
          locale: const Locale('de'),
          home: const Scaffold(
            body: LocalizedAutocomplete(),
          ),
        ),
      );

      // Enter search text
      await tester.enterText(find.byType(TextField), 'Deutsch');
      await tester.pumpAndSettle();

      // Should show German country name
      expect(find.text('Deutschland'), findsOneWidget);
    });
  });
}

Best Practices Summary

  1. Localize all text: Hints, labels, no-results, errors
  2. Handle diacritics: Allow searching without accents
  3. Support RTL: Proper alignment for Arabic/Hebrew
  4. Group results: Use localized category headers
  5. Announce changes: Screen reader accessibility
  6. Show loading states: Localized "Searching..." message
  7. Format results: Localized numbers, dates, currencies

Conclusion

Localizing autocomplete in Flutter requires attention to hints, suggestions, loading states, and accessibility. By implementing proper localized text, diacritic-aware search, and accessible announcements, you ensure users worldwide can efficiently find what they're looking for in your app.