Skip to content

Commit 20a2b75

Browse files
authored
Merge pull request #154 from contentstack/enhc/DX-4448
feat(CDA): Add asset localisation support
2 parents ef16659 + 02e3303 commit 20a2b75

File tree

5 files changed

+251
-1
lines changed

5 files changed

+251
-1
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
#### Date: Feb-10-2026
33

44
##### Feat:
5-
- CDA / DAM 2.0 – AssetFields support
5+
- CDA / – AssetFields support
66
- Added `AssetFields(params string[] fields)` to request specific asset-related metadata via the CDA `asset_fields[]` query parameter
77
- Implemented on: Entry (single entry fetch), Query (entries find), Asset (single asset fetch), AssetLibrary (assets find)
88
- Valid parameters: `user_defined_fields`, `embedded_metadata`, `ai_generated_metadata`, `visual_markups`
99
- Method is chainable; when called with no arguments, the query parameter is not set
10+
- CDA / – Asset localisation support
11+
- Added `SetLocale(string locale)` on Asset for single-asset fetch by locale (e.g. `stack.Asset(uid).SetLocale("en-us").Fetch()`)
12+
- Added `Title` property on Asset for localised title in API response
13+
- AssetLibrary `SetLocale` continues to support listing assets by locale
1014

1115
### Version: 2.25.2
1216
#### Date: Nov-13-2025

Contentstack.Core.Tests/AssetTest.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,5 +1133,124 @@ public async Task AssetFields_AssetLibrary_WithEmptyArray_RequestSucceeds()
11331133
Assert.Fail("AssetLibrary.FetchAll with AssetFields(empty array) did not return a result.");
11341134
Assert.NotNull(assets.Items);
11351135
}
1136+
1137+
[Fact]
1138+
public async Task FetchAssetsWithLocale_ReturnsLocalisedAssets()
1139+
{
1140+
var locale = "en-us"; // or "ar" if your stack has that locale
1141+
ContentstackCollection<Asset> assets = await client.AssetLibrary()
1142+
.SetLocale(locale)
1143+
.FetchAll();
1144+
1145+
Assert.True(assets.Items != null);
1146+
if (assets.Items.Count() == 0)
1147+
return; // no assets in this locale
1148+
1149+
foreach (Asset asset in assets)
1150+
{
1151+
// Root-level locale (when API returns it)
1152+
var rootLocale = asset.Get("locale");
1153+
if (rootLocale != null)
1154+
Assert.Equal(locale, rootLocale.ToString());
1155+
1156+
// Or via publish_details (existing pattern from FetchAssetsPublishWithoutFallback)
1157+
var publishDetails = asset.Get("publish_details") as JObject;
1158+
if (publishDetails != null && publishDetails["locale"] != null)
1159+
Assert.Equal(locale, publishDetails["locale"]?.ToString());
1160+
}
1161+
}
1162+
1163+
/// <summary>
1164+
/// Asset localisation: Fetch single asset with locale query param; response has requested locale.
1165+
/// </summary>
1166+
[Fact]
1167+
public async Task FetchSingleAssetWithLocale_ReturnsLocalisedAsset()
1168+
{
1169+
string uid = await FetchAssetUID();
1170+
var locale = "en-us";
1171+
1172+
Asset asset = await client.Asset(uid).AddParam("locale", locale).Fetch();
1173+
1174+
Assert.NotNull(asset);
1175+
Assert.NotNull(asset.Uid);
1176+
var publishDetails = asset.Get("publish_details") as JObject;
1177+
if (publishDetails != null && publishDetails["locale"] != null)
1178+
Assert.Equal(locale, publishDetails["locale"]?.ToString());
1179+
var rootLocale = asset.Get("locale");
1180+
if (rootLocale != null)
1181+
Assert.Equal(locale, rootLocale.ToString());
1182+
}
1183+
1184+
/// <summary>
1185+
/// Asset localisation: List assets with SetLocale("ar"); each asset has locale in response.
1186+
/// </summary>
1187+
[Fact]
1188+
public async Task FetchAssetsWithLocaleAr_ReturnsAssetsWithLocale()
1189+
{
1190+
ContentstackCollection<Asset> assets = await client.AssetLibrary()
1191+
.SetLocale("en-us")
1192+
.Limit(10)
1193+
.FetchAll();
1194+
1195+
Assert.NotNull(assets.Items);
1196+
if (assets.Items.Count() == 0)
1197+
return; // stack may not have assets in "ar"
1198+
1199+
foreach (Asset asset in assets)
1200+
{
1201+
var publishDetails = asset.Get("publish_details") as JObject;
1202+
if (publishDetails != null && publishDetails["locale"] != null)
1203+
Assert.Equal("en-us", publishDetails["locale"]?.ToString());
1204+
var rootLocale = asset.Get("locale");
1205+
if (rootLocale != null)
1206+
Assert.Equal("en-us", rootLocale.ToString());
1207+
}
1208+
}
1209+
1210+
/// <summary>
1211+
/// Asset localisation: SetLocale with IncludeFallback returns assets; locale in publish_details.
1212+
/// </summary>
1213+
[Fact]
1214+
public async Task FetchAssetsWithLocaleAndFallback_ReturnsLocalisedOrFallback()
1215+
{
1216+
var locale = "en-us";
1217+
ContentstackCollection<Asset> assets = await client.AssetLibrary()
1218+
.SetLocale(locale)
1219+
.IncludeFallback()
1220+
.Limit(10)
1221+
.FetchAll();
1222+
1223+
Assert.NotNull(assets.Items);
1224+
if (assets.Items.Count() == 0)
1225+
return;
1226+
1227+
foreach (Asset asset in assets)
1228+
{
1229+
var publishDetails = asset.Get("publish_details") as JObject;
1230+
Assert.NotNull(publishDetails);
1231+
Assert.NotNull(publishDetails["locale"]);
1232+
}
1233+
}
1234+
1235+
/// <summary>
1236+
/// Asset localisation: Single asset fetch using Asset.SetLocale returns localised asset.
1237+
/// </summary>
1238+
[Fact]
1239+
public async Task FetchSingleAssetWithSetLocale_ReturnsLocalisedAsset()
1240+
{
1241+
string uid = await FetchAssetUID();
1242+
var locale = "en-us";
1243+
1244+
Asset asset = await client.Asset(uid).SetLocale(locale).Fetch();
1245+
1246+
Assert.NotNull(asset);
1247+
Assert.NotNull(asset.Uid);
1248+
var publishDetails = asset.Get("publish_details") as JObject;
1249+
if (publishDetails != null && publishDetails["locale"] != null)
1250+
Assert.Equal(locale, publishDetails["locale"]?.ToString());
1251+
var rootLocale = asset.Get("locale");
1252+
if (rootLocale != null)
1253+
Assert.Equal(locale, rootLocale.ToString());
1254+
}
11361255
}
11371256
}

Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,43 @@ public void SetLocale_AddsQueryParameter()
333333
Assert.Equal(locale, urlQueries?["locale"]?.ToString());
334334
}
335335

336+
/// <summary>
337+
/// Asset localisation: SetLocale with locale "ar" adds locale query param for API.
338+
/// </summary>
339+
[Fact]
340+
public void SetLocale_ForAssetLocalisation_AddsLocaleQueryParameter()
341+
{
342+
var assetLibrary = CreateAssetLibrary();
343+
var locale = "ar";
344+
345+
AssetLibrary result = assetLibrary.SetLocale(locale);
346+
347+
Assert.NotNull(result);
348+
Assert.Same(assetLibrary, result);
349+
var urlQueriesField = typeof(AssetLibrary).GetField("UrlQueries",
350+
BindingFlags.NonPublic | BindingFlags.Instance);
351+
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(assetLibrary);
352+
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
353+
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
354+
}
355+
356+
/// <summary>
357+
/// SetLocale when called again updates the locale query param (overwrite).
358+
/// </summary>
359+
[Fact]
360+
public void SetLocale_UpdatesLocaleWhenCalledAgain()
361+
{
362+
var assetLibrary = CreateAssetLibrary();
363+
assetLibrary.SetLocale("en-us");
364+
assetLibrary.SetLocale("ar");
365+
366+
var urlQueriesField = typeof(AssetLibrary).GetField("UrlQueries",
367+
BindingFlags.NonPublic | BindingFlags.Instance);
368+
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(assetLibrary);
369+
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
370+
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
371+
}
372+
336373
#endregion
337374

338375
#region AddParam Tests

Contentstack.Core.Unit.Tests/AssetUnitTests.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,68 @@ public void AddParam_AddsQueryParameter()
533533

534534
#endregion
535535

536+
#region Asset Locale Tests (single-asset fetch with locale)
537+
538+
/// <summary>
539+
/// Asset.SetLocale adds locale query param for single-asset fetch.
540+
/// </summary>
541+
[Fact]
542+
public void SetLocale_AddsQueryParameter()
543+
{
544+
var asset = CreateAsset();
545+
var locale = "en-us";
546+
547+
Asset result = asset.SetLocale(locale);
548+
549+
Assert.NotNull(result);
550+
Assert.Same(asset, result);
551+
var urlQueriesField = typeof(Asset).GetField("UrlQueries",
552+
BindingFlags.NonPublic | BindingFlags.Instance);
553+
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
554+
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
555+
Assert.Equal("en-us", urlQueries?["locale"]?.ToString());
556+
}
557+
/// <summary>
558+
/// Asset localisation: AddParam("locale", "ar") adds locale query for single-asset fetch.
559+
/// </summary>
560+
[Fact]
561+
public void AddParam_WithLocale_AddsLocaleQueryParameter()
562+
{
563+
var asset = CreateAsset();
564+
var locale = "ar";
565+
566+
Asset result = asset.AddParam("locale", locale);
567+
568+
Assert.NotNull(result);
569+
Assert.Same(asset, result);
570+
var urlQueriesField = typeof(Asset).GetField("UrlQueries",
571+
BindingFlags.NonPublic | BindingFlags.Instance);
572+
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
573+
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
574+
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
575+
}
576+
577+
/// <summary>
578+
/// Single-asset fetch: locale can be combined with include_fallback.
579+
/// </summary>
580+
[Fact]
581+
public void AddParam_LocaleWithIncludeFallback_AddsBothQueryParameters()
582+
{
583+
var asset = CreateAsset();
584+
585+
asset.AddParam("locale", "en-us").IncludeFallback();
586+
587+
var urlQueriesField = typeof(Asset).GetField("UrlQueries",
588+
BindingFlags.NonPublic | BindingFlags.Instance);
589+
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
590+
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
591+
Assert.Equal("en-us", urlQueries?["locale"]?.ToString());
592+
Assert.True(urlQueries?.ContainsKey("include_fallback") ?? false);
593+
Assert.Equal("true", urlQueries?["include_fallback"]?.ToString());
594+
}
595+
596+
#endregion
597+
536598
#region SetHeader and RemoveHeader Tests
537599

538600
[Fact]

Contentstack.Core/Models/Asset.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ public string Url
118118
/// </summary>
119119
public string Description { get; set; }
120120

121+
/// <summary>
122+
/// Localized title of the asset (e.g. when fetched with SetLocale / asset localisation).
123+
/// </summary>
124+
[JsonProperty(PropertyName = "title")]
125+
public string Title { get; set; }
126+
121127
/// <summary>
122128
/// Set array of Tags
123129
/// </summary>
@@ -319,6 +325,28 @@ public Asset AssetFields(params string[] fields)
319325
return this;
320326
}
321327

328+
/// <summary>
329+
/// Sets the locale for fetching this asset. Returns the asset in the specified locale.
330+
/// </summary>
331+
/// <param name="locale">Locale code (e.g. "en-us", "ar").</param>
332+
/// <returns>Current instance of Asset for chaining.</returns>
333+
/// <example>
334+
/// <code>
335+
/// var asset = await stack.Asset(uid).SetLocale("en-us").Fetch();
336+
/// </code>
337+
/// </example>
338+
public Asset SetLocale(string locale)
339+
{
340+
if (!string.IsNullOrEmpty(locale))
341+
{
342+
if (UrlQueries.ContainsKey("locale"))
343+
UrlQueries["locale"] = locale;
344+
else
345+
UrlQueries.Add("locale", locale);
346+
}
347+
return this;
348+
}
349+
322350

323351
public void RemoveHeader(string key)
324352
{

0 commit comments

Comments
 (0)