-
Notifications
You must be signed in to change notification settings - Fork 12
Description
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:
- SDK configuration:
// main.dart
await Crowdin.init(
distributionHash: 'your_distribution_hash',
updatesInterval: Duration(minutes: 15),
);
await Crowdin.loadTranslations(Locale('fr'));- Create an ARB file without
@metadata(as downloaded from Crowdin CLI):
{
"@@locale": "fr",
"step_x_of_x": "Étape {currentStep} sur {totalSteps}"
}-
Upload to Crowdin and configure OTA distribution
-
Use the translation with parameters in your app:
final text = AppLocalizations.of(context).stepXOfX(1, 5);
print(text); // Expected: "Étape 1 sur 5"- 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);
}