stringsPutHandler function
PUT /api/strings/<key> — update a translation (or source) value.
Body shape:
{
"locale": "es",
"value": "Reservar ahora",
"locked": true, // optional; only meaningful when locale != source
"glossary_exempt": false // optional; only meaningful when locale == source
}
Side effects:
- The target ARB file at
dialect/translations/<locale>.arb(ordialect/source/<locale>.arbfor the source locale) is rewritten in canonical form viaArbWriter. The file mtime updates only when the desired bytes differ from disk (M5 idempotency contract). - Locking writes
@key.source_hashfrom the current source value perdialect/spec/source_hash.md. Unlocking clears it. - For the source locale, the request may set
glossary_exempton the@keyblock.
Implementation
Future<Response> stringsPutHandler(
DialectProject project,
Request req,
String key,
) async {
final bodyText = await req.readAsString();
Map<String, Object?> body;
try {
final decoded = jsonDecode(bodyText);
if (decoded is! Map<String, Object?>) {
return _jsonError(400, 'Body must be a JSON object.');
}
body = decoded;
} on FormatException catch (e) {
return _jsonError(400, 'Body is not valid JSON: ${e.message}');
}
final locale = body['locale'];
if (locale is! String || locale.isEmpty) {
return _jsonError(400, '`locale` is required and must be a string.');
}
final value = body['value'];
if (value is! String) {
return _jsonError(400, '`value` is required and must be a string.');
}
final lockedReq = body['locked'];
if (lockedReq != null && lockedReq is! bool) {
return _jsonError(400, '`locked` must be a boolean when provided.');
}
final exemptReq = body['glossary_exempt'];
if (exemptReq != null && exemptReq is! bool) {
return _jsonError(
400,
'`glossary_exempt` must be a boolean when provided.',
);
}
final isSource = locale == project.config.sourceLocale;
final isTarget = project.config.targetLocales.contains(locale);
if (!isSource && !isTarget) {
return _jsonError(404, 'Locale `$locale` is not configured.');
}
// Look up the source entry by key. PUT may create a missing
// translation but not a new source key (that's `dialect import`'s job).
final sourceEntry = project.source.entryFor(key);
if (sourceEntry == null) {
return _jsonError(404, 'Unknown source key `$key`.');
}
final updatedArb = isSource
? _applySourceUpdate(
project,
key: key,
value: value,
glossaryExempt: exemptReq as bool?,
)
: _applyTranslationUpdate(
project,
locale: locale,
key: key,
value: value,
sourceValue: sourceEntry.value,
locked: lockedReq as bool?,
);
// Write back via ArbWriter for canonical formatting.
final path = _arbPath(project, locale: locale, isSource: isSource);
final desired = ArbWriter.encode(updatedArb);
final file = File(path);
file.parent.createSync(recursive: true);
final currentBytes = file.existsSync() ? file.readAsStringSync() : null;
if (currentBytes != desired) {
file.writeAsStringSync(desired);
}
return Response.ok(
jsonEncode({'key': key, 'locale': locale, 'value': value}),
headers: const {'content-type': 'application/json; charset=utf-8'},
);
}