Skip to content

Commit

Permalink
Merge pull request #1840 from riganti/feature/route-localized-versions
Browse files Browse the repository at this point in the history
Added AlternateCultureLinks control
  • Loading branch information
exyi authored Aug 18, 2024
2 parents 315cf5f + 8a148e6 commit 6fb56a7
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/Framework/Framework/Controls/AlternateCultureLinks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using DotVVM.Framework.Configuration;
using DotVVM.Framework.Hosting;
using DotVVM.Framework.Routing;

namespace DotVVM.Framework.Controls
{
/// <summary>
/// Renders a <c>&lt;link rel=alternate</c> element for each localized route equivalent to the current route.
/// On non-localized routes, it renders nothing (the control is therefore safe to use in a master page).
/// The href must be an absolute URL, so it will only work correctly if <c>Context.Request.Url</c> contains the corrent domain.
/// </summary>
/// <seealso href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#alternate" />
/// <seealso href="https://developers.google.com/search/docs/specialty/international/localized-versions#html" />
/// <seealso cref="DotvvmRouteTable.Add(string, string, Type, object, LocalizedRouteUrl[])"/>
public class AlternateCultureLinks : CompositeControl
{
/// <param name="routeName">The name of the route to generate alternate links for. If not set, the current route is used. </param>
public IEnumerable<DotvvmControl> GetContents(IDotvvmRequestContext context, string? routeName = null)
{
var route = routeName != null ? context.Configuration.RouteTable[routeName] : context.Route;
if (route is LocalizedDotvvmRoute localizedRoute)
{
var currentCultureRoute = localizedRoute.GetRouteForCulture(CultureInfo.CurrentUICulture);

foreach (var alternateCultureRoute in localizedRoute.GetAllCultureRoutes())
{
if (alternateCultureRoute.Value == currentCultureRoute) continue;

var languageCode = alternateCultureRoute.Key == "" ? "x-default" : alternateCultureRoute.Key.ToLowerInvariant();
var alternateUrl = context.TranslateVirtualPath(alternateCultureRoute.Value.BuildUrl(context.Parameters!));
var absoluteAlternateUrl = BuildAbsoluteAlternateUrl(context, alternateUrl);

yield return new HtmlGenericControl("link")
.SetAttribute("rel", "alternate")
.SetAttribute("hreflang", languageCode)
.SetAttribute("href", absoluteAlternateUrl);
}

}
}

protected virtual string BuildAbsoluteAlternateUrl(IDotvvmRequestContext context, string alternateUrl)
{
return new Uri(context.HttpContext.Request.Url, alternateUrl).AbsoluteUri;
}
}
}
2 changes: 2 additions & 0 deletions src/Framework/Framework/Routing/LocalizedDotvvmRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public DotvvmRoute GetRouteForCulture(CultureInfo culture)
: throw new NotSupportedException("Invalid localized route - no default route found!");
}

public IReadOnlyDictionary<string, DotvvmRoute> GetAllCultureRoutes() => localizedRoutes;

public static void ValidateCultureName(string cultureIdentifier)
{
if (!AvailableCultureNames.Contains(cultureIdentifier))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<head>
<meta charset="utf-8" />
<title></title>

<dot:AlternateCultureLinks />
</head>
<body>

Expand Down
13 changes: 13 additions & 0 deletions src/Samples/Tests/Tests/Feature/LocalizationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ public void Feature_Localization_LocalizableRoute()
AssertUI.Attribute(links[1], "href", v => v.EndsWith("/de/FeatureSamples/Localization/lokalisierte-route"));
AssertUI.Attribute(links[2], "href", v => v.EndsWith("/FeatureSamples/Localization/LocalizableRoute"));
AssertUI.Attribute(links[3], "href", links[2].GetAttribute("href"));
AssertAlternateLink("cs-cz", "/cs/FeatureSamples/Localization/lokalizovana-routa");
AssertAlternateLink("de", "/de/FeatureSamples/Localization/lokalisierte-route");

links[0].Click().Wait(500);
culture = browser.Single("span[data-ui=culture]");
Expand All @@ -155,6 +157,8 @@ public void Feature_Localization_LocalizableRoute()
AssertUI.Attribute(links[1], "href", v => v.EndsWith("/de/FeatureSamples/Localization/lokalisierte-route"));
AssertUI.Attribute(links[2], "href", v => v.EndsWith("/FeatureSamples/Localization/LocalizableRoute"));
AssertUI.Attribute(links[3], "href", links[0].GetAttribute("href"));
AssertAlternateLink("x-default", "/FeatureSamples/Localization/LocalizableRoute");
AssertAlternateLink("de", "/de/FeatureSamples/Localization/lokalisierte-route");

links[1].Click().Wait(500);
culture = browser.Single("span[data-ui=culture]");
Expand All @@ -164,6 +168,8 @@ public void Feature_Localization_LocalizableRoute()
AssertUI.Attribute(links[1], "href", v => v.EndsWith("/de/FeatureSamples/Localization/lokalisierte-route"));
AssertUI.Attribute(links[2], "href", v => v.EndsWith("/FeatureSamples/Localization/LocalizableRoute"));
AssertUI.Attribute(links[3], "href", links[1].GetAttribute("href"));
AssertAlternateLink("x-default", "/FeatureSamples/Localization/LocalizableRoute");
AssertAlternateLink("cs-cz", "/cs/FeatureSamples/Localization/lokalizovana-routa");

links[2].Click().Wait(500);
culture = browser.Single("span[data-ui=culture]");
Expand All @@ -173,6 +179,13 @@ public void Feature_Localization_LocalizableRoute()
AssertUI.Attribute(links[1], "href", v => v.EndsWith("/de/FeatureSamples/Localization/lokalisierte-route"));
AssertUI.Attribute(links[2], "href", v => v.EndsWith("/FeatureSamples/Localization/LocalizableRoute"));
AssertUI.Attribute(links[3], "href", links[2].GetAttribute("href"));
AssertAlternateLink("cs-cz", "/cs/FeatureSamples/Localization/lokalizovana-routa");
AssertAlternateLink("de", "/de/FeatureSamples/Localization/lokalisierte-route");

void AssertAlternateLink(string culture, string url)
{
AssertUI.Attribute(browser.Single($"link[rel=alternate][hreflang={culture}]"), "href", this.TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + url);
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@
"mappingMode": "InnerElement"
}
},
"DotVVM.Framework.Controls.AlternateCultureLinks": {
"RouteName": {
"type": "System.String",
"onlyHardcoded": true
}
},
"DotVVM.Framework.Controls.AuthenticatedView": {
"AuthenticatedTemplate": {
"type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework",
Expand Down Expand Up @@ -2005,6 +2011,12 @@
"baseType": "DotVVM.Framework.Controls.Decorator, DotVVM.Framework",
"withoutContent": true
},
"DotVVM.Framework.Controls.AlternateCultureLinks": {
"assembly": "DotVVM.Framework",
"baseType": "DotVVM.Framework.Controls.CompositeControl, DotVVM.Framework",
"withoutContent": true,
"isComposite": true
},
"DotVVM.Framework.Controls.AuthenticatedView": {
"assembly": "DotVVM.Framework",
"baseType": "DotVVM.Framework.Controls.ConfigurableHtmlControl, DotVVM.Framework",
Expand Down

0 comments on commit 6fb56a7

Please sign in to comment.