Skip to content

Commit dedd153

Browse files
committed
Add SourcePluralText support for PO plural translation
- Add SourcePluralText property to ResourceEntry for storing msgid_plural - Update PoResourceReader to populate SourcePluralText from msgid_plural - Update PoResourceWriter to use SourcePluralText for round-trip preservation - Update TranslateCommand to translate Key→'one' form and SourcePluralText→'other' form for PO plurals
1 parent 9a98e14 commit dedd153

2 files changed

Lines changed: 102 additions & 15 deletions

File tree

Commands/TranslateCommand.cs

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -354,40 +354,111 @@ private async Task TranslateKeysAsync(
354354
// Check if this is a plural key
355355
if (key.IsPlural && key.PluralForms != null && key.PluralForms.Count > 0)
356356
{
357-
// Translate each plural form
358357
var translatedForms = new Dictionary<string, string>();
359358
var anyFromCache = false;
360359
var anyFromApi = false;
360+
var isPo = settings.GetBackendName().Equals("po", StringComparison.OrdinalIgnoreCase);
361361

362-
foreach (var (formName, formValue) in key.PluralForms)
362+
if (isPo && !string.IsNullOrEmpty(key.SourcePluralText))
363363
{
364-
if (string.IsNullOrWhiteSpace(formValue))
365-
continue;
364+
// PO format: translate msgid (singular) and msgid_plural (plural) separately
365+
// msgid (Key) → "one" form
366+
// msgid_plural (SourcePluralText) → "other" and remaining forms
366367

367-
var request = new TranslationRequest
368+
// Translate singular (msgid → "one")
369+
var singularRequest = new TranslationRequest
368370
{
369-
SourceText = formValue,
371+
SourceText = key.Key,
370372
SourceLanguage = sourceLanguage,
371373
TargetLanguage = targetLang,
372374
TargetLanguageName = targetLanguageInfo.Name,
373-
Context = $"Plural form '{formName}' of key '{key.Key}'"
375+
Context = $"Singular form of plural key"
374376
};
375377

376-
TranslationResponse response;
378+
TranslationResponse singularResponse;
379+
if (cache != null && cache.TryGet(singularRequest, provider.Name, out var cachedSingular))
380+
{
381+
singularResponse = cachedSingular!;
382+
anyFromCache = true;
383+
}
384+
else
385+
{
386+
singularResponse = await provider.TranslateAsync(singularRequest, cancellationToken);
387+
cache?.Store(singularRequest, singularResponse);
388+
anyFromApi = true;
389+
}
390+
translatedForms["one"] = singularResponse.TranslatedText;
377391

378-
if (cache != null && cache.TryGet(request, provider.Name, out var cachedResponse))
392+
// Translate plural (msgid_plural → "other")
393+
var pluralRequest = new TranslationRequest
379394
{
380-
response = cachedResponse!;
395+
SourceText = key.SourcePluralText,
396+
SourceLanguage = sourceLanguage,
397+
TargetLanguage = targetLang,
398+
TargetLanguageName = targetLanguageInfo.Name,
399+
Context = $"Plural form of plural key"
400+
};
401+
402+
TranslationResponse pluralResponse;
403+
if (cache != null && cache.TryGet(pluralRequest, provider.Name, out var cachedPlural))
404+
{
405+
pluralResponse = cachedPlural!;
381406
anyFromCache = true;
382407
}
383408
else
384409
{
385-
response = await provider.TranslateAsync(request, cancellationToken);
386-
cache?.Store(request, response);
410+
pluralResponse = await provider.TranslateAsync(pluralRequest, cancellationToken);
411+
cache?.Store(pluralRequest, pluralResponse);
387412
anyFromApi = true;
388413
}
414+
translatedForms["other"] = pluralResponse.TranslatedText;
389415

390-
translatedForms[formName] = response.TranslatedText;
416+
// For languages with more plural forms (few, many, zero, two),
417+
// copy the "other" form as a reasonable default
418+
foreach (var category in key.PluralForms.Keys)
419+
{
420+
if (!translatedForms.ContainsKey(category))
421+
{
422+
// Use singular for "one", plural for everything else
423+
translatedForms[category] = category == "one"
424+
? singularResponse.TranslatedText
425+
: pluralResponse.TranslatedText;
426+
}
427+
}
428+
}
429+
else
430+
{
431+
// Non-PO formats: translate each plural form value directly
432+
foreach (var (formName, formValue) in key.PluralForms)
433+
{
434+
if (string.IsNullOrWhiteSpace(formValue))
435+
continue;
436+
437+
var request = new TranslationRequest
438+
{
439+
SourceText = formValue,
440+
SourceLanguage = sourceLanguage,
441+
TargetLanguage = targetLang,
442+
TargetLanguageName = targetLanguageInfo.Name,
443+
Context = $"Plural form '{formName}' of key '{key.Key}'"
444+
};
445+
446+
TranslationResponse response;
447+
448+
if (cache != null && cache.TryGet(request, provider.Name, out var cachedResponse))
449+
{
450+
response = cachedResponse!;
451+
anyFromCache = true;
452+
}
453+
else
454+
{
455+
response = await provider.TranslateAsync(request, cancellationToken);
456+
cache?.Store(request, response);
457+
anyFromApi = true;
458+
}
459+
460+
translatedForms[formName] = response.TranslatedText;
461+
}
391462
}
392463

393464
// Update or add entry with plural forms
@@ -396,6 +467,8 @@ private async Task TranslateKeysAsync(
396467
targetDict[key.Key].IsPlural = true;
397468
targetDict[key.Key].PluralForms = translatedForms;
398469
targetDict[key.Key].Value = translatedForms.GetValueOrDefault("other") ?? translatedForms.Values.FirstOrDefault() ?? "";
470+
// Preserve SourcePluralText for PO format round-trip
471+
targetDict[key.Key].SourcePluralText = key.SourcePluralText;
399472
}
400473
else
401474
{
@@ -405,7 +478,9 @@ private async Task TranslateKeysAsync(
405478
Value = translatedForms.GetValueOrDefault("other") ?? translatedForms.Values.FirstOrDefault() ?? "",
406479
Comment = key.Comment,
407480
IsPlural = true,
408-
PluralForms = translatedForms
481+
PluralForms = translatedForms,
482+
// Preserve SourcePluralText for PO format round-trip
483+
SourcePluralText = key.SourcePluralText
409484
});
410485
}
411486

@@ -421,9 +496,14 @@ private async Task TranslateKeysAsync(
421496
else
422497
{
423498
// Regular (non-plural) translation
499+
// For PO format, the Key (msgid) IS the source text, not the Value (msgstr)
500+
var sourceText = settings.GetBackendName().Equals("po", StringComparison.OrdinalIgnoreCase)
501+
? key.Key
502+
: (key.Value ?? string.Empty);
503+
424504
var request = new TranslationRequest
425505
{
426-
SourceText = key.Value ?? string.Empty,
506+
SourceText = sourceText,
427507
SourceLanguage = sourceLanguage,
428508
TargetLanguage = targetLang,
429509
TargetLanguageName = targetLanguageInfo.Name

LocalizationManager.Core/Models/ResourceEntry.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ public class ResourceEntry
5454
/// </summary>
5555
public Dictionary<string, string>? PluralForms { get; set; }
5656

57+
/// <summary>
58+
/// Source text for plural forms (PO format: msgid_plural).
59+
/// Used for translation when the source differs from the key.
60+
/// For non-PO formats, this is typically null as PluralForms contains the source.
61+
/// </summary>
62+
public string? SourcePluralText { get; set; }
63+
5764
/// <summary>
5865
/// Indicates if this entry is empty/null.
5966
/// For plural entries, checks if all plural forms are empty.

0 commit comments

Comments
 (0)