Skip to content

OTA translations show literal placeholders instead of substituted values #77

@LeventeAsztalosAlpian

Description

@LeventeAsztalosAlpian

OTA translations show literal placeholders instead of substituted values

Describe the bug

When fetching translations via OTA (Over-The-Air updates), placeholder substitution fails and literal placeholder syntax (e.g., {currentStep}, {userName}) appears in the UI instead of the actual values passed to the localization method.

To Reproduce

Steps to reproduce the behavior:

  1. SDK configuration:
// main.dart
await Crowdin.init(
  distributionHash: 'your_distribution_hash',
  updatesInterval: Duration(minutes: 15),
);

await Crowdin.loadTranslations(Locale('fr'));
  1. Create an ARB file without @metadata (as downloaded from Crowdin CLI):
{
  "@@locale": "fr",
  "step_x_of_x": "Étape {currentStep} sur {totalSteps}"
}
  1. Upload to Crowdin and configure OTA distribution

  2. Use the translation with parameters in your app:

final text = AppLocalizations.of(context).stepXOfX(1, 5);
print(text); // Expected: "Étape 1 sur 5"
  1. Observe the output showing literal placeholders: "Étape {currentStep} sur {totalSteps}"

Expected behavior

The placeholder values should be substituted with the provided arguments:

  • Input: stepXOfX(1, 5)
  • Expected output: "Étape 1 sur 5"

The SDK's automatic placeholder inference (via Message._inferPlaceholders()) should detect {currentStep} and {totalSteps} from the translation string and make them available for substitution, even without explicit @metadata entries.

Environment

  • Crowdin Flutter SDK: 0.8.0
  • Flutter version: [e.g. 3.24.0]
  • Dart version: [e.g. 3.5.0]

Screenshots

Example showing the issue:

UI Display: "Étape {currentStep} sur {totalSteps}"
Expected:   "Étape 1 sur 5"

Root Cause

In lib/src/common/gen_l10n_types.dart, the Message._inferPlaceholders() method (lines ~508-534) correctly parses the translation string and creates Placeholder objects for detected placeholders. However, these inferred placeholders are stored in a local undeclaredPlaceholders map that is never merged into the instance's placeholders map.

void _inferPlaceholders(Map<LocaleInfo, String> filenames) {
  final Map<String, Placeholder> undeclaredPlaceholders = <String, Placeholder>{};
  
  // ... placeholder detection logic ...
  
  if (placeholder == null) {
    placeholder = Placeholder(resourceId, identifier, <String, Object?>{});
    undeclaredPlaceholders[identifier] = placeholder;  // ← Stored but never merged
  }
  
  // ← MISSING: placeholders.addAll(undeclaredPlaceholders);
}

When Extractor.findPlaceholders() runs, it iterates over message.placeholders, which is empty, so no substitution occurs.

Proposed Fix

Add one line at the end of _inferPlaceholders() in lib/src/common/gen_l10n_types.dart:

void _inferPlaceholders(Map<LocaleInfo, String> filenames) {
  final Map<String, Placeholder> undeclaredPlaceholders = <String, Placeholder>{};
  
  // ... existing code ...
  
  // Merge inferred placeholders into the instance's placeholders map
  placeholders.addAll(undeclaredPlaceholders);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions