Skip to content

Commit 5f5b14e

Browse files
committed
Fix plural translation for unsaved UI changes
- Add KeyTranslationMetadata to TranslateRequestDto for explicit key state - API uses KeyMetadata.IsPlural instead of inferring from string patterns - Editor passes KeyMetadata to TranslateDialog for both bulk and single key translate - KeyDetailDrawer creates cells for all languages when converting to plural
1 parent 01e55ba commit 5f5b14e

5 files changed

Lines changed: 91 additions & 13 deletions

File tree

cloud/src/LrmCloud.Api/Services/Translation/CloudTranslationService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,14 @@ public async Task<TranslateResponseDto> TranslateKeysAsync(
153153
// Process each key and target language
154154
foreach (var key in keys)
155155
{
156+
// Determine if key is plural: use KeyMetadata from client if provided (for unsaved UI changes),
157+
// otherwise fall back to database value
158+
var isPlural = request.KeyMetadata?.TryGetValue(key.KeyName, out var metadata) == true
159+
? metadata.IsPlural
160+
: key.IsPlural;
161+
156162
// Get plural forms to process (empty string for non-plural keys)
157-
var pluralForms = key.IsPlural
163+
var pluralForms = isPlural
158164
? new[] { "one", "other", "zero", "two", "few", "many" }
159165
: new[] { "" };
160166

cloud/src/LrmCloud.Shared/DTOs/Translation/TranslateRequestDto.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ public class TranslateRequestDto
6262
/// Defaults to true for CLI compatibility.
6363
/// </summary>
6464
public bool SaveToDatabase { get; set; } = true;
65+
66+
/// <summary>
67+
/// Optional metadata about keys being translated. When provided, overrides
68+
/// database values for key properties like IsPlural. This allows translating
69+
/// keys with unsaved UI changes.
70+
/// Key = resource key name, Value = metadata.
71+
/// </summary>
72+
public Dictionary<string, KeyTranslationMetadata>? KeyMetadata { get; set; }
73+
}
74+
75+
/// <summary>
76+
/// Metadata about a key being translated.
77+
/// </summary>
78+
public class KeyTranslationMetadata
79+
{
80+
/// <summary>
81+
/// Whether this key is plural. When true, translates all plural forms.
82+
/// Overrides the database value when provided.
83+
/// </summary>
84+
public bool IsPlural { get; set; }
6585
}
6686

6787
/// <summary>

cloud/src/LrmCloud.Web/Components/KeyDetailDrawer.razor

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,13 +422,14 @@
422422
// Initialize active forms
423423
_activePluralForms = new HashSet<string> { PluralForms.One, PluralForms.Other };
424424

425-
// Create empty "one" forms
425+
// Create empty cells for both "one" and "other" forms for ALL languages
426426
foreach (var lang in Languages)
427427
{
428-
var key = TranslationGridRow.GetKey(lang, PluralForms.One);
429-
if (!Row.Translations.ContainsKey(key))
428+
// Create "one" form if missing
429+
var oneKey = TranslationGridRow.GetKey(lang, PluralForms.One);
430+
if (!Row.Translations.ContainsKey(oneKey))
430431
{
431-
Row.Translations[key] = new TranslationCell
432+
Row.Translations[oneKey] = new TranslationCell
432433
{
433434
LanguageCode = lang,
434435
PluralForm = PluralForms.One,
@@ -437,6 +438,20 @@
437438
IsDirty = true
438439
};
439440
}
441+
442+
// Create "other" form if missing (for languages that had no translation)
443+
var otherKey = TranslationGridRow.GetKey(lang, PluralForms.Other);
444+
if (!Row.Translations.ContainsKey(otherKey))
445+
{
446+
Row.Translations[otherKey] = new TranslationCell
447+
{
448+
LanguageCode = lang,
449+
PluralForm = PluralForms.Other,
450+
Value = null,
451+
OriginalValue = null,
452+
IsDirty = true
453+
};
454+
}
440455
}
441456
}
442457
else

cloud/src/LrmCloud.Web/Components/TranslateDialog.razor

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@
184184
[Parameter]
185185
public Dictionary<string, string>? SourceTexts { get; set; }
186186

187+
/// <summary>
188+
/// Optional metadata about keys being translated. When provided, overrides database values
189+
/// for key properties like IsPlural. This allows translating keys with unsaved UI changes.
190+
/// </summary>
191+
[Parameter]
192+
public Dictionary<string, KeyTranslationMetadata>? KeyMetadata { get; set; }
193+
187194
private List<TranslationProviderDto> _providers = new();
188195
private string? _selectedProvider;
189196
private HashSet<string> _selectedLanguages = new();
@@ -304,7 +311,8 @@
304311
Overwrite = _overwrite,
305312
Context = _context,
306313
SaveToDatabase = false, // Return translations for UI preview, don't save yet
307-
SourceTexts = SourceTexts // Pass current UI values if provided
314+
SourceTexts = SourceTexts, // Pass current UI values if provided
315+
KeyMetadata = KeyMetadata // Pass key metadata (e.g., IsPlural) for unsaved UI changes
308316
};
309317

310318
_translationResult = await TranslationService.TranslateKeysAsync(ProjectId, request);

cloud/src/LrmCloud.Web/Pages/Projects/Editor.razor

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -563,13 +563,17 @@ else
563563
// Build source texts from current UI state (may include unsaved edits)
564564
var sourceTexts = BuildSourceTextsFromRows(rowList, _project.DefaultLanguage);
565565

566+
// Build key metadata to tell API about unsaved state (e.g., IsPlural)
567+
var keyMetadata = BuildKeyMetadataFromRows(rowList);
568+
566569
var parameters = new DialogParameters
567570
{
568571
{ nameof(TranslateDialog.ProjectId), ProjectId },
569572
{ nameof(TranslateDialog.SourceLanguage), _project.DefaultLanguage },
570573
{ nameof(TranslateDialog.AvailableLanguages), _languages },
571574
{ nameof(TranslateDialog.KeysToTranslate), rowList.Select(r => r.KeyName).ToList() },
572-
{ nameof(TranslateDialog.SourceTexts), sourceTexts }
575+
{ nameof(TranslateDialog.SourceTexts), sourceTexts },
576+
{ nameof(TranslateDialog.KeyMetadata), keyMetadata }
573577
};
574578

575579
var options = new DialogOptions
@@ -711,16 +715,22 @@ else
711715
{
712716
if (_project == null) return;
713717

718+
var rowList = new List<TranslationGridRow> { row };
719+
714720
// Build source texts from current UI state (may include unsaved edits)
715-
var sourceTexts = BuildSourceTextsFromRows(new List<TranslationGridRow> { row }, _project.DefaultLanguage);
721+
var sourceTexts = BuildSourceTextsFromRows(rowList, _project.DefaultLanguage);
722+
723+
// Build key metadata to tell API about unsaved state (e.g., IsPlural)
724+
var keyMetadata = BuildKeyMetadataFromRows(rowList);
716725

717726
var parameters = new DialogParameters
718727
{
719728
{ nameof(TranslateDialog.ProjectId), ProjectId },
720729
{ nameof(TranslateDialog.SourceLanguage), _project.DefaultLanguage },
721730
{ nameof(TranslateDialog.AvailableLanguages), _languages },
722731
{ nameof(TranslateDialog.KeysToTranslate), new List<string> { row.KeyName } },
723-
{ nameof(TranslateDialog.SourceTexts), sourceTexts }
732+
{ nameof(TranslateDialog.SourceTexts), sourceTexts },
733+
{ nameof(TranslateDialog.KeyMetadata), keyMetadata }
724734
};
725735

726736
var options = new DialogOptions
@@ -738,7 +748,7 @@ else
738748
{
739749
// Apply translations to the drawer row only (cells will update in place)
740750
// User must click "Apply" to stage changes, then "Save" to persist
741-
ApplyTranslationsToDrawerRow(row, translationResult);
751+
await ApplyTranslationsToDrawerRow(row, translationResult);
742752
}
743753
}
744754

@@ -747,7 +757,7 @@ else
747757
/// Unlike ApplyTranslationsToUI, this does NOT stage to _editedRows - the user must click Apply.
748758
/// Handles both non-plural and plural keys.
749759
/// </summary>
750-
private void ApplyTranslationsToDrawerRow(TranslationGridRow row, TranslateResponseDto translationResult)
760+
private async Task ApplyTranslationsToDrawerRow(TranslationGridRow row, TranslateResponseDto translationResult)
751761
{
752762
var appliedCount = 0;
753763

@@ -779,10 +789,10 @@ else
779789

780790
if (appliedCount > 0)
781791
{
782-
// Refresh the drawer's plural form display to show new cells
783-
_keyDetailDrawer?.RefreshActivePluralForms();
784792
Snackbar.Add($"Applied {appliedCount} translations. Click Apply to stage changes.", Severity.Success);
793+
// First update parent state, then refresh drawer to show new cells/values
785794
StateHasChanged();
795+
await InvokeAsync(() => _keyDetailDrawer?.RefreshActivePluralForms());
786796
}
787797
}
788798

@@ -824,6 +834,25 @@ else
824834
return sourceTexts;
825835
}
826836

837+
/// <summary>
838+
/// Builds a dictionary of key metadata from the current UI state.
839+
/// This tells the API about unsaved changes like IsPlural conversions.
840+
/// </summary>
841+
private Dictionary<string, KeyTranslationMetadata> BuildKeyMetadataFromRows(List<TranslationGridRow> rows)
842+
{
843+
var metadata = new Dictionary<string, KeyTranslationMetadata>();
844+
845+
foreach (var row in rows)
846+
{
847+
metadata[row.KeyName] = new KeyTranslationMetadata
848+
{
849+
IsPlural = row.IsPlural
850+
};
851+
}
852+
853+
return metadata;
854+
}
855+
827856
private async Task DeleteKey(TranslationGridRow row)
828857
{
829858
_pendingDeleteRows = new[] { row };

0 commit comments

Comments
 (0)