Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
#### Date: Feb-10-2026

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

### Version: 2.25.2
#### Date: Nov-13-2025
Expand Down
119 changes: 119 additions & 0 deletions Contentstack.Core.Tests/AssetTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,5 +1133,124 @@ public async Task AssetFields_AssetLibrary_WithEmptyArray_RequestSucceeds()
Assert.Fail("AssetLibrary.FetchAll with AssetFields(empty array) did not return a result.");
Assert.NotNull(assets.Items);
}

[Fact]
public async Task FetchAssetsWithLocale_ReturnsLocalisedAssets()
{
var locale = "en-us"; // or "ar" if your stack has that locale
ContentstackCollection<Asset> assets = await client.AssetLibrary()
.SetLocale(locale)
.FetchAll();

Assert.True(assets.Items != null);
if (assets.Items.Count() == 0)
return; // no assets in this locale

foreach (Asset asset in assets)
{
// Root-level locale (when API returns it)
var rootLocale = asset.Get("locale");
if (rootLocale != null)
Assert.Equal(locale, rootLocale.ToString());

// Or via publish_details (existing pattern from FetchAssetsPublishWithoutFallback)
var publishDetails = asset.Get("publish_details") as JObject;
if (publishDetails != null && publishDetails["locale"] != null)
Assert.Equal(locale, publishDetails["locale"]?.ToString());
}
}

/// <summary>
/// Asset localisation: Fetch single asset with locale query param; response has requested locale.
/// </summary>
[Fact]
public async Task FetchSingleAssetWithLocale_ReturnsLocalisedAsset()
{
string uid = await FetchAssetUID();
var locale = "en-us";

Asset asset = await client.Asset(uid).AddParam("locale", locale).Fetch();

Assert.NotNull(asset);
Assert.NotNull(asset.Uid);
var publishDetails = asset.Get("publish_details") as JObject;
if (publishDetails != null && publishDetails["locale"] != null)
Assert.Equal(locale, publishDetails["locale"]?.ToString());
var rootLocale = asset.Get("locale");
if (rootLocale != null)
Assert.Equal(locale, rootLocale.ToString());
}

/// <summary>
/// Asset localisation: List assets with SetLocale("ar"); each asset has locale in response.
/// </summary>
[Fact]
public async Task FetchAssetsWithLocaleAr_ReturnsAssetsWithLocale()
{
ContentstackCollection<Asset> assets = await client.AssetLibrary()
.SetLocale("ar")
.Limit(10)
.FetchAll();

Assert.NotNull(assets.Items);
if (assets.Items.Count() == 0)
return; // stack may not have assets in "ar"

foreach (Asset asset in assets)
{
var publishDetails = asset.Get("publish_details") as JObject;
if (publishDetails != null && publishDetails["locale"] != null)
Assert.Equal("ar", publishDetails["locale"]?.ToString());
var rootLocale = asset.Get("locale");
if (rootLocale != null)
Assert.Equal("ar", rootLocale.ToString());
}
}

/// <summary>
/// Asset localisation: SetLocale with IncludeFallback returns assets; locale in publish_details.
/// </summary>
[Fact]
public async Task FetchAssetsWithLocaleAndFallback_ReturnsLocalisedOrFallback()
{
var locale = "en-us";
ContentstackCollection<Asset> assets = await client.AssetLibrary()
.SetLocale(locale)
.IncludeFallback()
.Limit(10)
.FetchAll();

Assert.NotNull(assets.Items);
if (assets.Items.Count() == 0)
return;

foreach (Asset asset in assets)
{
var publishDetails = asset.Get("publish_details") as JObject;
Assert.NotNull(publishDetails);
Assert.NotNull(publishDetails["locale"]);
}
}

/// <summary>
/// Asset localisation: Single asset fetch using Asset.SetLocale returns localised asset.
/// </summary>
[Fact]
public async Task FetchSingleAssetWithSetLocale_ReturnsLocalisedAsset()
{
string uid = await FetchAssetUID();
var locale = "ar";

Asset asset = await client.Asset(uid).SetLocale(locale).Fetch();

Assert.NotNull(asset);
Assert.NotNull(asset.Uid);
var publishDetails = asset.Get("publish_details") as JObject;
if (publishDetails != null && publishDetails["locale"] != null)
Assert.Equal(locale, publishDetails["locale"]?.ToString());
var rootLocale = asset.Get("locale");
if (rootLocale != null)
Assert.Equal(locale, rootLocale.ToString());
}
}
}
37 changes: 37 additions & 0 deletions Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,43 @@ public void SetLocale_AddsQueryParameter()
Assert.Equal(locale, urlQueries?["locale"]?.ToString());
}

/// <summary>
/// Asset localisation: SetLocale with locale "ar" adds locale query param for API.
/// </summary>
[Fact]
public void SetLocale_ForAssetLocalisation_AddsLocaleQueryParameter()
{
var assetLibrary = CreateAssetLibrary();
var locale = "ar";

AssetLibrary result = assetLibrary.SetLocale(locale);

Assert.NotNull(result);
Assert.Same(assetLibrary, result);
var urlQueriesField = typeof(AssetLibrary).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(assetLibrary);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
}

/// <summary>
/// SetLocale when called again updates the locale query param (overwrite).
/// </summary>
[Fact]
public void SetLocale_UpdatesLocaleWhenCalledAgain()
{
var assetLibrary = CreateAssetLibrary();
assetLibrary.SetLocale("ar");
assetLibrary.SetLocale("en-us");

var urlQueriesField = typeof(AssetLibrary).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(assetLibrary);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
}

#endregion

#region AddParam Tests
Expand Down
62 changes: 62 additions & 0 deletions Contentstack.Core.Unit.Tests/AssetUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,68 @@ public void AddParam_AddsQueryParameter()

#endregion

#region Asset Locale Tests (single-asset fetch with locale)

/// <summary>
/// Asset.SetLocale adds locale query param for single-asset fetch.
/// </summary>
[Fact]
public void SetLocale_AddsQueryParameter()
{
var asset = CreateAsset();
var locale = "en-us";

Asset result = asset.SetLocale(locale);

Assert.NotNull(result);
Assert.Same(asset, result);
var urlQueriesField = typeof(Asset).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("en-us", urlQueries?["locale"]?.ToString());
}
/// <summary>
/// Asset localisation: AddParam("locale", "ar") adds locale query for single-asset fetch.
/// </summary>
[Fact]
public void AddParam_WithLocale_AddsLocaleQueryParameter()
{
var asset = CreateAsset();
var locale = "ar";

Asset result = asset.AddParam("locale", locale);

Assert.NotNull(result);
Assert.Same(asset, result);
var urlQueriesField = typeof(Asset).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
}

/// <summary>
/// Single-asset fetch: locale can be combined with include_fallback.
/// </summary>
[Fact]
public void AddParam_LocaleWithIncludeFallback_AddsBothQueryParameters()
{
var asset = CreateAsset();

asset.AddParam("locale", "en-us").IncludeFallback();

var urlQueriesField = typeof(Asset).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("en-us", urlQueries?["locale"]?.ToString());
Assert.True(urlQueries?.ContainsKey("include_fallback") ?? false);
Assert.Equal("true", urlQueries?["include_fallback"]?.ToString());
}

#endregion

#region SetHeader and RemoveHeader Tests

[Fact]
Expand Down
28 changes: 28 additions & 0 deletions Contentstack.Core/Models/Asset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ public string Url
/// </summary>
public string Description { get; set; }

/// <summary>
/// Localized title of the asset (e.g. when fetched with SetLocale / asset localisation).
/// </summary>
[JsonProperty(PropertyName = "title")]
public string Title { get; set; }

/// <summary>
/// Set array of Tags
/// </summary>
Expand Down Expand Up @@ -319,6 +325,28 @@ public Asset AssetFields(params string[] fields)
return this;
}

/// <summary>
/// Sets the locale for fetching this asset. Returns the asset in the specified locale.
/// </summary>
/// <param name="locale">Locale code (e.g. "en-us", "ar").</param>
/// <returns>Current instance of Asset for chaining.</returns>
/// <example>
/// <code>
/// var asset = await stack.Asset(uid).SetLocale("en-us").Fetch();
/// </code>
/// </example>
public Asset SetLocale(string locale)
{
if (!string.IsNullOrEmpty(locale))
{
if (UrlQueries.ContainsKey("locale"))
UrlQueries["locale"] = locale;
else
UrlQueries.Add("locale", locale);
}
return this;
}


public void RemoveHeader(string key)
{
Expand Down
Loading