Skip to content

Commit 8033e1e

Browse files
committed
Add SourcePluralText support to cloud platform
- Add source_plural_text column to ResourceKey entity with EF migration - Add SourcePluralText to ResourceKeyDto and sync DTOs (EntryChangeDto, EntryDataDto) - Update FileImportService to extract SourcePluralText from PO msgid_plural - Update FileExportService to include SourcePluralText for round-trip export - Update GitHubPullService to store SourcePluralText when syncing from GitHub - Update KeySyncService to handle SourcePluralText in CLI push/pull sync - Update ResourceService DTO mappings to include SourcePluralText - Update editor UI to display source plural text in KeyDetailDrawer and KeyEditorDialog - Update Editor.razor to populate SourcePluralText in TranslationGridRow
1 parent dedd153 commit 8033e1e

16 files changed

Lines changed: 2678 additions & 4 deletions

cloud/src/LrmCloud.Api/Data/Migrations/20251229072700_AddSourcePluralTextToResourceKey.Designer.cs

Lines changed: 2462 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Microsoft.EntityFrameworkCore.Migrations;
2+
3+
#nullable disable
4+
5+
namespace LrmCloud.Api.Data.Migrations
6+
{
7+
/// <inheritdoc />
8+
public partial class AddSourcePluralTextToResourceKey : Migration
9+
{
10+
/// <inheritdoc />
11+
protected override void Up(MigrationBuilder migrationBuilder)
12+
{
13+
migrationBuilder.AddColumn<string>(
14+
name: "source_plural_text",
15+
table: "resource_keys",
16+
type: "text",
17+
nullable: true);
18+
}
19+
20+
/// <inheritdoc />
21+
protected override void Down(MigrationBuilder migrationBuilder)
22+
{
23+
migrationBuilder.DropColumn(
24+
name: "source_plural_text",
25+
table: "resource_keys");
26+
}
27+
}
28+
}

cloud/src/LrmCloud.Api/Data/Migrations/AppDbContextModelSnapshot.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,10 @@ protected override void BuildModel(ModelBuilder modelBuilder)
12011201
.HasColumnType("integer")
12021202
.HasColumnName("project_id");
12031203

1204+
b.Property<string>("SourcePluralText")
1205+
.HasColumnType("text")
1206+
.HasColumnName("source_plural_text");
1207+
12041208
b.Property<DateTime>("UpdatedAt")
12051209
.HasColumnType("timestamp with time zone")
12061210
.HasColumnName("updated_at");

cloud/src/LrmCloud.Api/Services/FileExportService.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
using LocalizationManager.Core.Backends.Android;
33
using LocalizationManager.Core.Backends.iOS;
44
using LocalizationManager.Core.Backends.Json;
5+
using LocalizationManager.Core.Backends.Po;
56
using LocalizationManager.Core.Backends.Resx;
7+
using LocalizationManager.Core.Backends.Xliff;
68
using LocalizationManager.Core.Configuration;
79
using LocalizationManager.Core.Models;
810
using LrmCloud.Api.Data;
@@ -151,7 +153,10 @@ private static ResourceFile BuildResourceFile(
151153
Value = pluralForms.GetValueOrDefault("other", ""),
152154
Comment = key.Comment,
153155
IsPlural = true,
154-
PluralForms = pluralForms
156+
PluralForms = pluralForms,
157+
// For PO format: SourcePluralText is msgid_plural
158+
// For other formats: this is the "other" plural form from source language
159+
SourcePluralText = key.SourcePluralText
155160
});
156161
}
157162
else
@@ -192,6 +197,8 @@ private static IResourceWriter GetWriter(string format)
192197
"i18next" => new JsonResourceWriter(new JsonFormatConfiguration { I18nextCompatible = true }),
193198
"android" => new AndroidResourceWriter(),
194199
"ios" => new IosResourceWriter(),
200+
"po" or "gettext" => new PoResourceWriter(),
201+
"xliff" or "xlf" => new XliffResourceWriter(),
195202
_ => throw new NotSupportedException($"Format '{format}' is not supported for export")
196203
};
197204
}
@@ -235,6 +242,16 @@ private static string GetFilePath(string basePath, string format, string languag
235242
// iOS: {basePath}/{lang}.lproj/Localizable.strings
236243
"ios" => $"{basePath}/{IosCultureMapper.CodeToLproj(hasLanguageCode ? languageCode : "Base")}/Localizable.strings",
237244

245+
// PO: {basePath}/messages.pot for default, {basePath}/{lang}.po for translations
246+
"po" or "gettext" => useDefaultPath
247+
? $"{basePath}/messages.pot"
248+
: $"{basePath}/{languageCode}.po",
249+
250+
// XLIFF: {basePath}/messages.xliff for default, {basePath}/messages.{lang}.xliff for translations
251+
"xliff" or "xlf" => useDefaultPath
252+
? $"{basePath}/messages.xliff"
253+
: $"{basePath}/messages.{languageCode}.xliff",
254+
238255
_ => throw new NotSupportedException($"Format '{format}' is not supported")
239256
};
240257
}

cloud/src/LrmCloud.Api/Services/FileImportService.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
using LocalizationManager.Core.Backends.Android;
33
using LocalizationManager.Core.Backends.iOS;
44
using LocalizationManager.Core.Backends.Json;
5+
using LocalizationManager.Core.Backends.Po;
56
using LocalizationManager.Core.Backends.Resx;
7+
using LocalizationManager.Core.Backends.Xliff;
68
using LocalizationManager.Core.Cloud;
79
using LocalizationManager.Core.Configuration;
810
using LocalizationManager.Core.Models;
@@ -67,6 +69,9 @@ public ParsedResourceFile ParseFile(
6769
Comment = entry.Comment,
6870
IsPlural = true,
6971
PluralForms = entry.PluralForms,
72+
// For PO: SourcePluralText is msgid_plural
73+
// For other formats: use "other" form from source language
74+
SourcePluralText = entry.SourcePluralText ?? entry.PluralForms.GetValueOrDefault("other"),
7075
Hash = pluralHash
7176
});
7277
}
@@ -207,6 +212,8 @@ private void ParseIosFilesWithPlurals(
207212
Comment = null,
208213
IsPlural = true,
209214
PluralForms = entry.PluralForms,
215+
// iOS stringsdict: use "other" form as source plural text
216+
SourcePluralText = entry.PluralForms.GetValueOrDefault("other"),
210217
Hash = pluralHash
211218
};
212219
}
@@ -247,6 +254,8 @@ private void ParseIosFilesWithPlurals(
247254
"i18next" => DetectI18nextLanguage(fileName, defaultLanguage),
248255
"android" => DetectAndroidLanguage(dirName, defaultLanguage),
249256
"ios" => DetectIosLanguage(dirName, defaultLanguage),
257+
"po" or "gettext" => DetectPoLanguage(filePath, defaultLanguage),
258+
"xliff" or "xlf" => DetectXliffLanguage(filePath, defaultLanguage),
250259
_ => throw new NotSupportedException($"Format '{format}' is not supported for import")
251260
};
252261
}
@@ -373,6 +382,74 @@ private static (string languageCode, bool isDefault) DetectIosLanguage(string di
373382
return (normalizedDefault, true);
374383
}
375384

385+
/// <summary>
386+
/// Detects language from PO file path.
387+
/// locale/fr/LC_MESSAGES/messages.po → fr
388+
/// po/fr.po → fr
389+
/// messages.pot → default (template)
390+
/// </summary>
391+
private static (string languageCode, bool isDefault) DetectPoLanguage(string filePath, string defaultLanguage)
392+
{
393+
var normalizedDefault = NormalizeLanguageCode(defaultLanguage);
394+
var fileName = Path.GetFileName(filePath);
395+
396+
// POT files are templates (default language)
397+
if (fileName.EndsWith(".pot", StringComparison.OrdinalIgnoreCase))
398+
return (normalizedDefault, true);
399+
400+
// Check for GNU gettext structure: locale/{lang}/LC_MESSAGES/...
401+
var parts = filePath.Replace("\\", "/").Split('/');
402+
for (int i = 0; i < parts.Length - 1; i++)
403+
{
404+
if (parts[i].Equals("locale", StringComparison.OrdinalIgnoreCase) ||
405+
parts[i].Equals("locales", StringComparison.OrdinalIgnoreCase))
406+
{
407+
if (i + 1 < parts.Length)
408+
{
409+
var lang = NormalizeLanguageCode(parts[i + 1]);
410+
return (lang, lang.Equals(normalizedDefault, StringComparison.Ordinal));
411+
}
412+
}
413+
}
414+
415+
// Check for flat structure: {lang}.po
416+
var langFromFileName = Path.GetFileNameWithoutExtension(fileName);
417+
if (!string.IsNullOrEmpty(langFromFileName) && langFromFileName.Length <= 5)
418+
{
419+
var lang = NormalizeLanguageCode(langFromFileName);
420+
return (lang, lang.Equals(normalizedDefault, StringComparison.Ordinal));
421+
}
422+
423+
return (normalizedDefault, true);
424+
}
425+
426+
/// <summary>
427+
/// Detects language from XLIFF file path or content.
428+
/// messages.fr.xliff → fr
429+
/// Extracts trgLang/target-language from content if available.
430+
/// </summary>
431+
private static (string languageCode, bool isDefault) DetectXliffLanguage(string filePath, string defaultLanguage)
432+
{
433+
var normalizedDefault = NormalizeLanguageCode(defaultLanguage);
434+
var fileName = Path.GetFileName(filePath);
435+
var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
436+
437+
// Check for language code in filename: messages.fr.xliff or messages.fr.xlf
438+
var parts = nameWithoutExt.Split('.');
439+
if (parts.Length >= 2)
440+
{
441+
var possibleLang = parts[^1]; // Last part before extension
442+
if (possibleLang.Length <= 5) // Reasonable language code length
443+
{
444+
var lang = NormalizeLanguageCode(possibleLang);
445+
return (lang, lang.Equals(normalizedDefault, StringComparison.Ordinal));
446+
}
447+
}
448+
449+
// Default to project default language
450+
return (normalizedDefault, true);
451+
}
452+
376453
/// <summary>
377454
/// Gets the appropriate reader for the format.
378455
/// </summary>
@@ -385,6 +462,8 @@ private static IResourceReader GetReader(string format)
385462
"i18next" => new JsonResourceReader(new JsonFormatConfiguration { I18nextCompatible = true }),
386463
"android" => new AndroidResourceReader(),
387464
"ios" => new IosResourceReader(),
465+
"po" or "gettext" => new PoResourceReader(),
466+
"xliff" or "xlf" => new XliffResourceReader(),
388467
_ => throw new NotSupportedException($"Format '{format}' is not supported for import")
389468
};
390469
}

cloud/src/LrmCloud.Api/Services/GitHubPullService.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ public class GitHubPullService : IGitHubPullService
3030
["json"] = new[] { ".json" },
3131
["i18next"] = new[] { ".json" },
3232
["android"] = new[] { "strings.xml" },
33-
["ios"] = new[] { "Localizable.strings", "Localizable.stringsdict" }
33+
["ios"] = new[] { "Localizable.strings", "Localizable.stringsdict" },
34+
["po"] = new[] { ".po", ".pot" },
35+
["gettext"] = new[] { ".po", ".pot" },
36+
["xliff"] = new[] { ".xliff", ".xlf" },
37+
["xlf"] = new[] { ".xliff", ".xlf" }
3438
};
3539

3640
public GitHubPullService(
@@ -703,11 +707,18 @@ private async Task ApplyChangesAsync(int projectId, MergeResult result, int user
703707
ProjectId = projectId,
704708
KeyName = entry.Key,
705709
IsPlural = entry.IsPlural,
706-
Comment = entry.Comment
710+
Comment = entry.Comment,
711+
// For plural keys, store the source plural text (PO msgid_plural or "other" form)
712+
SourcePluralText = entry.IsPlural ? entry.SourcePluralText : null
707713
};
708714
_db.ResourceKeys.Add(key);
709715
await _db.SaveChangesAsync();
710716
}
717+
else if (entry.IsPlural && key.SourcePluralText == null && entry.SourcePluralText != null)
718+
{
719+
// Update SourcePluralText if not set yet
720+
key.SourcePluralText = entry.SourcePluralText;
721+
}
711722

712723
// Upsert translation with "pending" status
713724
await UpsertTranslationAsync(

cloud/src/LrmCloud.Api/Services/IFileImportService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ public class GitHubEntry
1212
public string? Comment { get; init; }
1313
public bool IsPlural { get; init; }
1414
public Dictionary<string, string>? PluralForms { get; init; }
15+
/// <summary>
16+
/// For plural keys, the source plural text pattern (PO msgid_plural or "other" form).
17+
/// </summary>
18+
public string? SourcePluralText { get; init; }
1519
public required string Hash { get; init; }
1620
}
1721

cloud/src/LrmCloud.Api/Services/KeySyncService.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,8 @@ public async Task<KeySyncPullResponse> PullAsync(
234234
{
235235
Key = key.KeyName,
236236
Comment = key.Comment,
237-
IsPlural = key.IsPlural
237+
IsPlural = key.IsPlural,
238+
SourcePluralText = key.SourcePluralText
238239
};
239240

240241
if (key.IsPlural)
@@ -381,6 +382,8 @@ private async Task<EntryChangeResult> ApplyEntryChangeAsync(
381382
KeyName = entry.Key,
382383
IsPlural = entry.IsPlural,
383384
Comment = entry.Comment,
385+
// For plural keys, store source plural text (PO msgid_plural or "other" form)
386+
SourcePluralText = entry.IsPlural ? entry.SourcePluralText : null,
384387
Version = 1,
385388
CreatedAt = DateTime.UtcNow,
386389
UpdatedAt = DateTime.UtcNow
@@ -396,6 +399,12 @@ private async Task<EntryChangeResult> ApplyEntryChangeAsync(
396399
resourceKey.IsPlural = entry.IsPlural;
397400
resourceKey.UpdatedAt = DateTime.UtcNow;
398401
}
402+
// Update SourcePluralText if not set yet
403+
if (entry.IsPlural && resourceKey.SourcePluralText == null && entry.SourcePluralText != null)
404+
{
405+
resourceKey.SourcePluralText = entry.SourcePluralText;
406+
resourceKey.UpdatedAt = DateTime.UtcNow;
407+
}
399408
}
400409

401410
// For plural entries, store each plural form as a separate Translation row

cloud/src/LrmCloud.Api/Services/ResourceService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,7 @@ private ResourceKeyDto MapToResourceKeyDto(ResourceKey key)
854854
KeyName = key.KeyName,
855855
KeyPath = key.KeyPath,
856856
IsPlural = key.IsPlural,
857+
SourcePluralText = key.SourcePluralText,
857858
Comment = key.Comment,
858859
Version = key.Version,
859860
TranslationCount = key.Translations.Count,
@@ -875,6 +876,7 @@ private ResourceKeyDetailDto MapToResourceKeyDetailDto(ResourceKey key, string?
875876
KeyName = key.KeyName,
876877
KeyPath = key.KeyPath,
877878
IsPlural = key.IsPlural,
879+
SourcePluralText = key.SourcePluralText,
878880
Comment = key.Comment,
879881
Version = key.Version,
880882
TranslationCount = key.Translations.Count,

cloud/src/LrmCloud.Shared/DTOs/Resources/ResourceKeyDto.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ public class ResourceKeyDto
99
public required string KeyName { get; set; }
1010
public string? KeyPath { get; set; }
1111
public bool IsPlural { get; set; }
12+
/// <summary>
13+
/// For plural keys, the source plural text pattern (PO msgid_plural or "other" form).
14+
/// </summary>
15+
public string? SourcePluralText { get; set; }
1216
public string? Comment { get; set; }
1317
public int Version { get; set; }
1418
public int TranslationCount { get; set; }

0 commit comments

Comments
 (0)