Skip to content

Commit 4063313

Browse files
committed
Refactor cloud sync with language code normalization
- Normalize language codes during pull operations for format compatibility - Remove redundant code from push/pull commands - Add file regeneration support for language code changes - Improve CloudSyncValidator output formatting
1 parent e0791f2 commit 4063313

8 files changed

Lines changed: 182 additions & 202 deletions

File tree

Commands/Cloud/CloneCommand.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,11 @@ private int PullResources(string targetDirectory, RemoteUrl remoteUrl, CloudConf
483483
AnsiConsole.MarkupLine($"[dim]Found {pullResponse.Total} entries[/]");
484484

485485
// Convert entries to MergedEntry format for FileRegenerator
486+
// Determine if we should normalize default language to ""
487+
// XLIFF/iOS/i18next use explicit language codes, so don't normalize for those
488+
var normalizeDefaultLang = KeyLevelMerger.BackendUsesEmptyForDefault(backend.Name);
486489
var merger = new KeyLevelMerger();
487-
var mergeResult = merger.MergeForFirstPull(pullResponse.Entries);
490+
var mergeResult = merger.MergeForFirstPull(pullResponse.Entries, pullResponse.DefaultLanguage, normalizeDefaultLang);
488491

489492
if (mergeResult.ToWrite.Count == 0)
490493
{
@@ -494,12 +497,14 @@ private int PullResources(string targetDirectory, RemoteUrl remoteUrl, CloudConf
494497
}
495498

496499
// Get languages from the entries
500+
// Note: After normalization, default language may be "" (for resx/android) or explicit code (for xliff/ios)
497501
var languages = mergeResult.ToWrite.Select(e => e.Lang).Distinct()
498502
.Select(lang => new Core.Models.LanguageInfo
499503
{
500504
Code = lang,
501-
Name = lang,
502-
IsDefault = lang == remoteProject.DefaultLanguage
505+
Name = string.IsNullOrEmpty(lang) ? remoteProject.DefaultLanguage : lang,
506+
IsDefault = string.IsNullOrEmpty(lang) ||
507+
string.Equals(lang, remoteProject.DefaultLanguage, StringComparison.OrdinalIgnoreCase)
503508
})
504509
.ToList();
505510

Commands/Cloud/PullCommand.cs

Lines changed: 35 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using Spectre.Console;
88
using Spectre.Console.Cli;
99
using System.ComponentModel;
10-
using System.Text.Json;
1110

1211
namespace LocalizationManager.Commands.Cloud;
1312

@@ -36,16 +35,6 @@ public class PullCommandSettings : BaseCommandSettings
3635
[DefaultValue("prompt")]
3736
public string Strategy { get; set; } = "prompt";
3837

39-
[CommandOption("--config-only")]
40-
[Description("Pull only configuration (lrm.json), skip resources")]
41-
[DefaultValue(false)]
42-
public bool ConfigOnly { get; set; }
43-
44-
[CommandOption("--resources-only")]
45-
[Description("Pull only resources, skip configuration")]
46-
[DefaultValue(false)]
47-
public bool ResourcesOnly { get; set; }
48-
4938
[CommandOption("--include-unapproved")]
5039
[Description("Include translations that haven't been approved yet (when project has review workflow enabled)")]
5140
[DefaultValue(false)]
@@ -95,13 +84,6 @@ public override int Execute(CommandContext context, PullCommandSettings settings
9584
AnsiConsole.MarkupLine($"[dim]Remote: {remoteUrl!.ToString().EscapeMarkup()}[/]");
9685
AnsiConsole.WriteLine();
9786

98-
// Validate conflicting options
99-
if (settings.ConfigOnly && settings.ResourcesOnly)
100-
{
101-
AnsiConsole.MarkupLine("[red]✗ Cannot use --config-only and --resources-only together![/]");
102-
return 1;
103-
}
104-
10587
// Parse resolution strategy
10688
var (strategy, isValidStrategy) = ParseStrategy(settings.Strategy);
10789
if (!isValidStrategy)
@@ -154,10 +136,9 @@ public override int Execute(CommandContext context, PullCommandSettings settings
154136
return 1;
155137
}
156138

157-
// Load local config if exists
139+
// Load local config if exists for validation
158140
ConfigurationModel? localConfig = null;
159-
var localConfigPath = Path.Combine(projectDirectory, "lrm.json");
160-
if (File.Exists(localConfigPath))
141+
if (File.Exists(Path.Combine(projectDirectory, "lrm.json")))
161142
{
162143
localConfig = Core.Configuration.ConfigurationManager.LoadConfigurationAsync(projectDirectory, cancellationToken).GetAwaiter().GetResult();
163144
}
@@ -203,16 +184,25 @@ public override int Execute(CommandContext context, PullCommandSettings settings
203184
AnsiConsole.MarkupLine("[yellow]⚠ Upgrading from file-based sync to key-level sync[/]");
204185
}
205186

206-
// Get backend for the format
187+
// Get backend for the local format (client-agnostic: format is determined locally)
207188
var backendFactory = new Core.Backends.ResourceBackendFactory();
208189
Core.Abstractions.IResourceBackend backend;
209190
try
210191
{
211-
backend = backendFactory.GetBackend(remoteProject!.Format, config);
192+
// Use local config format or auto-detect from files
193+
if (!string.IsNullOrEmpty(config.ResourceFormat))
194+
{
195+
backend = backendFactory.GetBackend(config.ResourceFormat, config);
196+
}
197+
else
198+
{
199+
backend = backendFactory.ResolveFromPath(projectDirectory, config);
200+
}
212201
}
213-
catch (NotSupportedException)
202+
catch (NotSupportedException ex)
214203
{
215-
AnsiConsole.MarkupLine($"[red]✗ Unsupported format: {remoteProject.Format}[/]");
204+
AnsiConsole.MarkupLine($"[red]✗ {ex.Message}[/]");
205+
AnsiConsole.MarkupLine("[dim]Set 'format' in lrm.json or ensure resource files exist.[/]");
216206
return 1;
217207
}
218208

@@ -243,36 +233,33 @@ public override int Execute(CommandContext context, PullCommandSettings settings
243233
AnsiConsole.MarkupLine($"[dim]Remote: {pullResponse.Total} entries[/]");
244234

245235
// Perform three-way merge
236+
// Pass default language to normalize API language codes to CLI convention
237+
// (CLI uses "" for default language, API uses actual language code like "en")
238+
// But XLIFF/iOS/i18next use explicit language codes, so don't normalize for those
246239
var merger = new KeyLevelMerger();
247240
MergeResult mergeResult;
248241

242+
// Determine if we should normalize default language to ""
243+
// If we have local entries, check if they use "" for default
244+
// Otherwise, use the backend's convention
245+
var normalizeDefaultLang = localEntries.Any()
246+
? localEntries.Any(e => string.IsNullOrEmpty(e.Lang))
247+
: KeyLevelMerger.BackendUsesEmptyForDefault(backend.Name);
248+
249249
if (syncState == null || !syncState.Entries.Any())
250250
{
251251
// First pull - accept all remote
252-
mergeResult = merger.MergeForFirstPull(pullResponse.Entries);
252+
mergeResult = merger.MergeForFirstPull(pullResponse.Entries, pullResponse.DefaultLanguage, normalizeDefaultLang);
253253
AnsiConsole.MarkupLine("[dim]First pull - accepting all remote entries[/]");
254254
}
255255
else
256256
{
257-
// Normal merge
258-
mergeResult = merger.MergeForPull(localEntries, pullResponse.Entries, syncState);
259-
}
260-
261-
// Handle config merge
262-
ConfigMergeResult? configMergeResult = null;
263-
if (!settings.ResourcesOnly && pullResponse.Config != null)
264-
{
265-
var configMerger = new ConfigMerger();
266-
var localConfigJson = File.Exists(localConfigPath) ? File.ReadAllText(localConfigPath) : null;
267-
var localProps = localConfigJson != null
268-
? configMerger.ExtractConfigProperties(localConfigJson)
269-
: new Dictionary<string, (string Value, string Hash)>();
270-
271-
configMergeResult = configMerger.MergeForPull(localProps, pullResponse.Config, syncState);
257+
// Normal merge - uses local entries to detect convention internally
258+
mergeResult = merger.MergeForPull(localEntries, pullResponse.Entries, syncState, pullResponse.DefaultLanguage);
272259
}
273260

274261
// Show changes summary
275-
ShowMergeSummary(mergeResult, configMergeResult, settings);
262+
ShowMergeSummary(mergeResult);
276263

277264
// Handle conflicts
278265
if (mergeResult.HasConflicts)
@@ -327,7 +314,7 @@ public override int Execute(CommandContext context, PullCommandSettings settings
327314
}
328315

329316
// Check if there's anything to do
330-
if (mergeResult.ToWrite.Count == 0 && configMergeResult?.ToWrite.Count == 0)
317+
if (mergeResult.ToWrite.Count == 0)
331318
{
332319
AnsiConsole.MarkupLine("[green]✓ Already up to date![/]");
333320
return 0;
@@ -364,30 +351,10 @@ public override int Execute(CommandContext context, PullCommandSettings settings
364351
// Apply changes
365352
try
366353
{
367-
// Update config FIRST so backend uses correct settings for file regeneration
368-
if (!settings.ResourcesOnly && configMergeResult?.ToWrite.Count > 0)
369-
{
370-
AnsiConsole.Status()
371-
.Start("Updating configuration...", ctx =>
372-
{
373-
var configMerger = new ConfigMerger();
374-
var localConfigJson = File.Exists(localConfigPath) ? File.ReadAllText(localConfigPath) : "{}";
375-
var newConfigJson = configMerger.ApplyConfigChanges(localConfigJson, configMergeResult.ToWrite);
376-
File.WriteAllText(localConfigPath, newConfigJson);
377-
});
378-
379-
// Reload config and backend after config update
380-
config = Core.Configuration.ConfigurationManager.LoadConfigurationAsync(projectDirectory, cancellationToken).GetAwaiter().GetResult();
381-
backend = backendFactory.GetBackend(remoteProject!.Format, config);
382-
// Re-discover languages with updated config
383-
languages = backend.Discovery.DiscoverLanguages(projectDirectory);
384-
}
385-
386354
AnsiConsole.Status()
387355
.Start("Applying changes...", ctx =>
388356
{
389-
// Update resources (now with correct backend from updated config)
390-
if (!settings.ConfigOnly && mergeResult.ToWrite.Count > 0)
357+
if (mergeResult.ToWrite.Count > 0)
391358
{
392359
ctx.Status("Regenerating resource files...");
393360
var regenerator = new FileRegenerator(backend, projectDirectory);
@@ -404,7 +371,7 @@ public override int Execute(CommandContext context, PullCommandSettings settings
404371
});
405372

406373
// Update sync state
407-
var newSyncState = BuildNewSyncState(mergeResult, configMergeResult, localEntries);
374+
var newSyncState = BuildNewSyncState(mergeResult, localEntries);
408375
SyncStateManager.SaveAsync(projectDirectory, newSyncState, cancellationToken).GetAwaiter().GetResult();
409376

410377
AnsiConsole.WriteLine();
@@ -418,10 +385,6 @@ public override int Execute(CommandContext context, PullCommandSettings settings
418385
{
419386
AnsiConsole.MarkupLine($" [dim]Auto-merged: {mergeResult.AutoMerged} entries[/]");
420387
}
421-
if (configMergeResult?.ToWrite.Count > 0)
422-
{
423-
AnsiConsole.MarkupLine($" [dim]Config properties: {configMergeResult.ToWrite.Count}[/]");
424-
}
425388

426389
if (backupPath != null)
427390
{
@@ -471,7 +434,7 @@ public override int Execute(CommandContext context, PullCommandSettings settings
471434
}
472435
}
473436

474-
private void ShowMergeSummary(MergeResult mergeResult, ConfigMergeResult? configMergeResult, PullCommandSettings settings)
437+
private void ShowMergeSummary(MergeResult mergeResult)
475438
{
476439
AnsiConsole.MarkupLine("[blue]Merge summary:[/]");
477440

@@ -487,14 +450,6 @@ private void ShowMergeSummary(MergeResult mergeResult, ConfigMergeResult? config
487450
{
488451
AnsiConsole.MarkupLine($" [yellow]! {mergeResult.Conflicts.Count} conflict(s) need resolution[/]");
489452
}
490-
if (configMergeResult?.ToWrite.Count > 0)
491-
{
492-
AnsiConsole.MarkupLine($" [blue]~ {configMergeResult.ToWrite.Count} config property(ies) to update[/]");
493-
}
494-
if (configMergeResult?.Conflicts.Count > 0)
495-
{
496-
AnsiConsole.MarkupLine($" [yellow]! {configMergeResult.Conflicts.Count} config conflict(s)[/]");
497-
}
498453

499454
AnsiConsole.WriteLine();
500455
}
@@ -643,14 +598,13 @@ private bool ResolveConflictsInteractively(KeyLevelMerger merger, MergeResult me
643598
};
644599
}
645600

646-
private SyncState BuildNewSyncState(MergeResult mergeResult, ConfigMergeResult? configMergeResult, List<LocalEntry> localEntries)
601+
private SyncState BuildNewSyncState(MergeResult mergeResult, List<LocalEntry> localEntries)
647602
{
648603
var newState = new SyncState
649604
{
650605
Version = 2,
651606
Timestamp = DateTime.UtcNow,
652-
Entries = new Dictionary<string, Dictionary<string, string>>(),
653-
ConfigProperties = new Dictionary<string, string>()
607+
Entries = new Dictionary<string, Dictionary<string, string>>()
654608
};
655609

656610
// Add hashes from merged entries
@@ -686,15 +640,6 @@ private SyncState BuildNewSyncState(MergeResult mergeResult, ConfigMergeResult?
686640
}
687641
}
688642

689-
// Add config hashes
690-
if (configMergeResult != null)
691-
{
692-
foreach (var (path, hash) in configMergeResult.NewHashes)
693-
{
694-
newState.ConfigProperties[path] = hash;
695-
}
696-
}
697-
698643
return newState;
699644
}
700645
}

0 commit comments

Comments
 (0)