Getting Started with Localization

Topics: Developer Forum
Oct 13, 2008 at 7:50 PM
I did a quick search but couldn't find much on localization. Is there a good example, tutorial, or walk-through on taking existing content and then providing translations so that when a user visits a page the content is shown in the correct language (or a default if it isn't translated to that language)?
Oct 13, 2008 at 9:05 PM
try searching for globalisation.
A basic start if you are not using the template project is here: http://www.codeplex.com/n2/Thread/View.aspx?ThreadId=36599
Oct 14, 2008 at 12:26 PM
Thanks for the link. I followed the instructions in thread 36599 and I now see a language drop-down when I go to edit content. To test, I created a Hello World page, selecting "English (United States)" as the language but I do not see an obvious way to add a translation. What do I do next?

Thank you -

-James
Oct 14, 2008 at 3:11 PM
There comes the beauty!
Select the page, or a node above it, or the root node and hit the button on the top toolbar with the green globe...that you'll know!
Oct 14, 2008 at 3:29 PM
I don't understand - when I click the globe it just shows "English (United States)" and under that the page I want to translate ("Hello World"). There are two buttons: Associate and Unassociate. The Associate button gives an error "Seleact two or more items to associate" and the Unassociate button only refreshes the page.

Where do I add the translation? What is "Associate" supposed to do - associate what with what? What other "items" should I be seeing to "associate" with?

I find this interface very confusing :-\.
Oct 14, 2008 at 5:25 PM
Ah, you did not have another LanguageRoot item yet.
Add another LanguageRoot to the same level as the one you had for the English one. On this new languageroot select the desired second language.
Now try the globe again. You will see that the second language is shown.

The globalisation is set up to have different content for different languages.
This way you are able to have different pages for the languages (or the same if you desire so).
The associate options links two page from two different languages together. After that N2 knows they are translations of each other.
Oct 14, 2008 at 6:01 PM
Hi MartijnRasenberg,

Thank you very much for your help so far. Unfortunarly, I still need a little more help :-(.

> On this new languageroot select the desired second language.
Now try the globe again. You will see that the second language is shown.

This seemed to work at first - but now EVERY page I add shows as a translation of my initial page, even if it is in the same language.

From the beginning:
  1. I added a page named "Hello World" using the language "English (United States)"
  2. I added a second page named "Hola Mundo" at the same level as "Hello World" and set the language to "Spanish (Mexico)".
  3. "Hola Mundo" was then automatically as the translation for "Hello World" even though I did not assign it.
  4. I added an additional page called "Page Two" using the language "English (United States)"
  5. I added an additional page called "Pagina Dos" using the language "Spanish (Mexico)"
  6. I clicked the green globe - now all four pages are listed on the same line by "Hello World" as translations of the same page!
How do I assign "Pagina Dos" as the spanish version of "Page Two" but not as the Spanish version of "Hello World"?

I tried clicking "Unassociate" however it does nothing. If I click "Associate" I now get an error "Seleact two or more items to associate" - but there is no way to "select" items to associate. What am I missing?


Oct 14, 2008 at 7:19 PM
How is your site's structure set up?

Is it something like:

Root
  - LanguageRoot English
    - Hello World
    - Page Two
  - LanguageRoot Spanish
    - Hola Mundo
    - Pagina Dos

It should be like this. The translations only work when the are under a LanguageRoot, which is a page implementing ILanguage (see thread 36599).
Oct 14, 2008 at 7:30 PM
Edited Oct 14, 2008 at 7:36 PM
> Is it something like:

This is not how I did it, however I re-arranged the structure and tried again. I got the same result. :-(

The translations page looks like this:

English         | English     | English  | Spanish         | Spanish    | Spanish    |
--------------------------------------------------------------------------------------
English Content | Hello World | Page One | Spanish Content | Hola Mundo | Pagina Dos |
--------------------------------------------------------------------------------------
The site structure is what you suggested.
Coordinator
Oct 14, 2008 at 8:50 PM
It might be possible to confuse the language system about which pages are translations of which. In such cases you will have to unassociate the pages and then associate them again.

When you created "Hola Mundo" did you you click on "Hello World" and choose the spanish flag?
Oct 14, 2008 at 8:52 PM
It looks like you have implemented ILanguage on all contentitems.
You should implement it only on the LanguageRoot...
Oct 15, 2008 at 12:30 PM
But I want all content to be capable of translation... if everything does not implement ILanguage then not everything can be translated. This approach does not work.
Oct 15, 2008 at 12:54 PM
it works for me
Oct 15, 2008 at 12:59 PM
> it works for me

It does not work in the templates example. I have made a new post summarizing the problems with this approach.
Oct 15, 2008 at 1:26 PM
> When you created "Hola Mundo" did you you click on "Hello World" and choose the spanish flag?

How do I do that? There is no way I can see to make this selection.
Oct 15, 2008 at 2:03 PM
Why another thread...that's not really useful??
You actually gave some extra info in the new thread...which probably would be useful to solve your problem here.

Do you have one (1) start page / root node?
"The idea is that only nodes that are below the start page (/) can be accessed through a friendly URL. The others are still accessible for convenience." (see http://www.codeplex.com/n2/Thread/View.aspx?ThreadId=36534).
The site structure must be something like this:
-Root
  - LanguageRoot English
    - Hello World
    - Page Two
  - LanguageRoot Spanish
    - Hola Mundo
    - Pagina Dos

Only pages below the root-node / startpage which is defined in the web.config are accessible through friendly URL's.
For Globlisation to work the structure must be a-like.
Oct 15, 2008 at 2:56 PM
> Why another thread...that's not really useful??

Because I am seeking a valid example from which I can determine the best way forward; I am not finding this discussion very useful in actually solving the problem of globalization. I think an example is best, but I cannot find one (as I said, the templates example does not function correctly).

> Do you have one (1) start page / root node?

Yes.

> For Globlisation to work the structure must be a-like.

This is not a valid solution for several reasons:
  • It alters the URL structure of the site. This is bad because:
    • It changes the structure of an existing websites (it will break existing bookmarks, for example)
    • A site with the URL /en/topic.aspx is viewed as less important by search engines than a site with simply /topic.aspx as the URL because "/en/topic.aspx" it is deeper within the site and search engines typically rank more shallow URLs  higher.
  • You cannot translate child-pages of custom page types that are not language roots. The obvious solution would be to make all page types language roots however, as we discussed, this causes N2 to think every page is a translation of every other page which is of course unusable.
This implementation allows only the very simple sites to be localized. Most of the framework is exceptionally well designed, so I feel as though I must be missing something. I believe a working example is the best way to illustrate what I am missing (if I am indeed missing something).
Coordinator
Oct 15, 2008 at 5:43 PM
Hi James, it's not clear how you would prefer the URL structure. E.g. when you refer to /topic.aspx how would a visitor (or a search spider) reach the english and spanish translations?
Coordinator
Oct 15, 2008 at 5:48 PM
Can you elaborate on "can't translate child-pages of non language roots". Why? The language root is just a way of telling the CMS which languages are available (one per language). Then it will help out to synchronize translations which are created below each language root respectively.
Oct 15, 2008 at 6:23 PM
> Hi James, it's not clear how you would prefer the URL structure. E.g. when you refer to /topic.aspx how would a visitor (or a search spider) reach the english and spanish translations?

Two solutions are ok with me:
  1. Give me control of the structure by not requiring a "root" for translations (this requirement seems arbitrary to begin with). A simple collection of translation URLs on language-specific pages would be adequate; this collection should only allow one translation per language (the current implementation apparently allows multiple translations to the same language - you can translate English US to English US for example).
  2. Automatically detect the language of the client browser and show the correct language for that browser (or a default if no translation is available).
Ideally, both options would be available. For example: I may want the start page, or pages using product names, to be dynamic but inner pages to use language-specific URLs.

Examples of what I mean:
mysite.com - this should show the correct language automatically, based on the request language.

mysite.com/about-us.aspx - this should show in English because it is an English name where as mysite.com/acerca-de-nosotros.aspx would show the spanish language "about us" content.

mysite.com/N2.aspx - this should show the correct language automatically, based on the request language because "N2" is not specific to a language (it is a proper name).
If it must be one or the other, then option one is best: Simply eliminate the need for a "root" and let the content creators decide which pages are translations of other pages.

> Can you elaborate on "can't translate child-pages of non language roots".

If I have this structure:
  • Language Root
    • Regular Page (not language root)
      • Child of Regular Page
"Child of Regular Page" cannot be translated. This is illustrated in the Templates example - none of the pages under /Home/Features can be translated.

> The language root is just a way of telling the CMS which languages are available (one per language). Then it will help out to synchronize translations which are created below each language root respectively.

But why require a root? Why not simply enable the users to specify which pages are translations of other pages?
Oct 15, 2008 at 6:43 PM
>> Why another thread...that's not really useful??
>Because I am seeking a valid example from which I can determine the best way forward; I am not finding this discussion very useful in actually solving the problem of globalization. I think an example is best, but I cannot find one (as I said, the templates example does not function correctly).

Well, sorry for trying to help you in answering your questions. You did not really make it clear that you had a problem with the globalisation-setup chosen for N2...
I hope you will come to enjoy N2 and make something useful of it.
Oct 15, 2008 at 6:54 PM
> Well, sorry for trying to help you in answering your questions.

I did not mean to offend. Only that our discussion, though useful for understanding N2, has not actually led to the successful globalization of my website. In other words, I have not met the goal I have set out to meet. Please don't take it to mean I don't appreciate your help, I appreciate it very much.

> I hope you will come to enjoy N2 and make something useful of it.

I am enjoying it quite a lot! The design is very elegant, and implementation is mostly very simple. I do feel, however, that the globalization feature is more complicated than it needs to be (and is counter-intuitive).
Coordinator
Oct 15, 2008 at 8:32 PM
I'll have to disagree with you on your view of friendly URLs. I think having the same URL for several languages would confuse visitors and search spiders even more.

As for the intuitiveness of the implementation I can only agree with you and I appreciate you bring it up. This needs some documentation explaining how it works. Some benefits of having it this way:
  • It doesn't increase the complexity of the content item, those that don't globalize should hardly notice it at all
  • It's possible to diverge the content structure without loosing globalization help for the translated parts
  • The url structure is very flexible
Oct 15, 2008 at 10:30 PM
> I'll have to disagree with you on your view of friendly URLs. I think having the same URL for several languages would confuse visitors and search spiders even more.

This was the recommendation we received from our SEO agency. They are very good, and we trust them. They also suggested, however, that visitors should be able to override the default language by setting a session variable (it is ok, in this case, to use the query string).

> This needs some documentation explaining how it works.

What is the best way to contribute this? I think I can help here! :-)

> The url structure is very flexible

I do not see why translations must be "forced" into a sub-directory. It seems as though the more flexible approach would be to allow users to define the structure. If this structural limitation were removed, it would make a great deal of difference.

I finally have everything working and I am very happy :-). The only problem is I will not pass quality assurance tests when it is discovered that all Spanish translated pages are under the "/es" directory. If there is any way to put two different languages in one parent, I'd love to know how. For example: I want mysite.com/hello-world.aspx and mysite.com/hola-mundo.aspx (the translation of hello-world) both to work if this is possible.

Thank you!
Coordinator
Oct 16, 2008 at 7:25 PM
Allright then =) This is what happens when you request a page, and it's possible to hook into (and potentially solve the problem) each step:

 - The current start page is determined
 - The url is passed to the start page's GetChild
 - If the first url segment match a sub-page the remaining url is passed that page's GetChild method
 - If a page is found it's stored in the request context.
 - If no page was UrlParser.PageNotFound is fired
 - The request is rewritten to the current page's RewrittenUrl

In your situation you probably should hook into the first step: determine start page.

Create a class implementing N2.Web.IUrlParser or inherit from one of the existing. Create some logic to determine start page, e.g. looking at browser language or find the page in either of the languages. Register it as a component in web.config: <n2><engine><services><components><add service="interface" implementation="class"

You can help by writing about it in the wiki: http://n2cms.com/wiki.aspx
Nov 6, 2008 at 1:49 PM
@MartijnRasenberg: I'm trying to build the structure you're outlining (using MVC). I can't seem to get the several language roots at the root level (i.e. as children of root). N2 seems to insist on this structure:
-Root
  -LanguageRoot (en)
    -LanguageRoot (dk)
    -LanguageRoot (cn)

... while the structure I want is this:
-Root
  -LanguageRoot (en)
  -LanguageRoot (dk)
  -LanguageRoot (cn)

I'm worried that the first structure will make a mess of URL's but maybe I'm wrong?

Kind Regards
Michael
Nov 6, 2008 at 2:23 PM
> -Root
>   -LanguageRoot (en)
>   -LanguageRoot (dk)
>   -LanguageRoot (cn)

To get this structure you need to enable multi-site configuration and each language root will have to exist on a different domain (e.g. en.yoursite.com, dk.yoursite.com, cn.yoursite.com). Is that what you intended?

If you want yoursite.com/en, yoursite.com/dk, and so forth then you can just have your Start Page redirect based on the end-users language setting so the structure would be:

-Root
  -NonLanguageRootWithRedirectToCorrectLanguageRoot
    -LanguageRoot (en)
    -LanguageRoot (dk)
    -LanguageRoot (cn)
Nov 6, 2008 at 2:26 PM
Got it, thanks a bunch.
Jan 23, 2009 at 4:08 PM
@jamestharpe, @librado

following the structre for the site:

-Root
  -NonLanguageRootWithRedirectToCorrectLanguageRoot
    -LanguageRoot (en)
    -LanguageRoot (dk)
    -LanguageRoot (cn)

what would be the best way to implement the "NonLanguageRootWithRedirectToCorrectLanguageRoot"? Would be any way to implement it directly in the Root item, that could be deactivated and forward the user to a language, based on their browser, or to a default one in case that the locale for the browser language is not available?

I am not sure if there is an easy way to implement the "-NonLanguageRootWithRedirectToCorrectLanguageRoot" without sending a redirect response to the client. Could you please give a tip about the best way to implement that? would be great to have it integrated in the official release of the N2 CMS.

Thanks in advance for your support.
Pedro




Coordinator
Jan 23, 2009 at 8:56 PM
You could probably do this by overrideing the FindPath method on NonLanguageRootWithRedirectToCorrectLanguageRoot:

override PathData FindPath(string remainingUrl)
{
    if(remainingUrl == "/")
       return GetPathDependingOnBrowserLanguage();
    return base.FindPath()
}

where GetPathDependingOnBrowserLanguage would try to determine the correct language and return a PathData pointing to one of the child items.
Mar 6, 2009 at 4:08 PM
Thanks again for your feedback. I've been implementing this feature and I still have a problem. I can't resolve the language of an item from the item itself. There is no access to the Engine, like it happens in the Master Class as well (http://n2.codeplex.com/WorkItem/View.aspx?WorkItemId=21507). The whole class is here:

namespace BusinessEntities.Models
{
    [N2.Definition("Root page with redirection to browser language")]
    [AllowedChildren(Types = new Type[] { typeof(LanguageRootItem) })]
    public class RootWithRedirectToCorrectLanguageRoot : N2.ContentItem
    {
        public override PathData FindPath(string remainingUrl)
        {
            if (remainingUrl == "/")
            {
                return GetPathDependingOnBrowserLanguage();
            }

            return base.FindPath (remainingUrl);
        }

        private static PathData GetPathDependingOnBrowserLanguage()
        {
            // Iterate through the user browser languages to get the root item
            var cultures = ResolveCultures();
            foreach (var culture in cultures)
            {
                var pathData = GetLanguageRootItem(culture);
                if (pathData == null) continue;
                return pathData;
            }
            return null;
        }

        private static PathData GetLanguageRootItem(CultureInfo culture)
        {
            var rootLanguageItems = N2.Find.Items.Where.Type.Eq(typeof (LanguageRootItem)).Select();
            
            foreach (var rootLanguageItem in rootLanguageItems)
            {
                // TODO: Fix to get the languageCode instead the title
                if (culture.Name.ToLower() == rootLanguageItem.Title)
                    return new PathData(rootLanguageItem, rootLanguageItem.TemplateUrl);
            }

            return null;
        }


        public static RegionInfo ResolveCountry(CultureInfo culture)
        {
            return culture != null ? new RegionInfo(culture.LCID) : null;
        }

        private static IEnumerable<CultureInfo> ResolveCultures()
        {
            // TODO: Move to web.config
            const string defaultLanguage = "en-gb";
            
            var languages = HttpContext.Current.Request.UserLanguages;
            var userLanguages = HttpContext.Current.Request.UserLanguages;

            var languagesList = new List<CultureInfo>();
            CultureInfo c;
            if (userLanguages != null)
            {
                foreach(var lang in languages)
                {
                    c = ResolveCutlure(lang);
                    if (c != null) languagesList.Add(c);
                 }
            }

            // Add the default language
            c = ResolveCutlure(defaultLanguage);
            if (c != null) languagesList.Add(c);

            return languagesList;
        }

        private static CultureInfo ResolveCutlure(string lang)
        {
            var tokens = lang.Split(';');

            string cleanLang = tokens[0].ToLowerInvariant().Trim();

            CultureInfo culture;
            try
            {
                culture = CultureInfo.CreateSpecificCulture(cleanLang);
            }
            catch (Exception)
            {
                culture = null;
            }

            return culture;
        }
    }
}

Coordinator
Mar 6, 2009 at 9:56 PM
It might not be the most beautiful solution but you can access the engine through a static singleton: N2.Context.Current.
Mar 9, 2009 at 11:52 AM
Thanks, it worked. I changed

                // TODO: Fix to get the languageCode instead the title
                if (culture.Name.ToLower() == rootLanguageItem.Title)
                    return new PathData(rootLanguageItem, rootLanguageItem.TemplateUrl);

for:

            var rootLanguageItems = N2.Find.Items.Where.Type.Eq(typeof (LanguageRootItem)).Select();
            var languages = N2.Context.Current.Resolve<ILanguageGateway>();

            foreach (var rootLanguageItem in rootLanguageItems)
            {
                if (languages.GetLanguage(rootLanguageItem).LanguageCode.ToLower() == culture.Name.ToLower())
                    return new PathData(rootLanguageItem, rootLanguageItem.TemplateUrl);
            }



Sep 29, 2010 at 12:40 PM

Hi!:

I'm newbie using N2 CMS and I have a question about Globalization. I installed the whole website (via IIS) and it has some pages, in English and translated to  Swedish and everything (almost) works fine if I add new content page and apply "automatic" translation. The new pages are under "/sv/" but if I add new LanguageRoot under Start Page and then use language menu to translate the existing pages to new one (to Spanish) those new pages aren't not hanging under "/es/" as I expected.

I've created a new MVC Website Project and then I've added N2 following the steps I could find here (that's was not as easy as I thought but I finally got it) without using N2 template library. Now I'm trying to add globalization and I get to implement LanguageRoot and create new translation, and the question is: How do I get that new translations url are under language code folder (/en/home.aspx & /es/inicio.aspx).

What I get is the next tree

-[Root]

-Start Page (added at some installation point)

-Home (en): -> /home.aspx

-About (en): -> /about.aspx

-Spanish Language Root (LanguageRoot type)

-Inicio(es): -> /inicio.aspx

-Nosotros (es): -> /nosotros.aspx

All of them have url friendly, but I need new pages have url friendly under the language code folder like that: "www.mysite.com/es/inicio.aspx" or "www.mysite.com/fr/accueil.aspx".

My implementation for ILanguage is similar to the website I downloaded:

 //[Definition("Language root", "LanguageRoot", "A starting point for translations of the start page.", "", 450)]
    [PageDefinition("Language root",
        Description = "A starting point for translations of the start page.",
        SortOrder = 450,
        IconUrl = "~/Content/Img/page_world.png")]
    [RestrictParents(typeof(StartPage))]
    public class LanguageRoot : AbstractContentPageIStructuralPageILanguageIStartPage
    {
        public LanguageRoot()
        {
            Visible = false;
            SortOrder = 10000;
        }

        public const string SiteArea = "siteArea";
        

        #region ILanguage Members

        public string FlagUrl
        {
            get
            {
                if (string.IsNullOrEmpty(LanguageCode))
                    return "";
                else
                {
                    string[] parts = LanguageCode.Split('-');
                    return string.Format("~/N2/Resources/Img/Flags/{0}.png"parts[parts.Length - 1]);
                }
            }
        }

        [EditableLanguagesDropDown("Language"100)]
        public string LanguageCode
        {
            get { return (string)GetDetail("LanguageCode"); }
            set { SetDetail("LanguageCode"value); }
        }

        public string LanguageTitle
        {
            get
            {
                if (string.IsNullOrEmpty(LanguageCode))
                    return "";
                else
                    return new CultureInfo(LanguageCode).DisplayName;
            }
        }

        #endregion

       
        
    }

Could you help me, please?

 

Thanks in advance :-D

Sep 29, 2010 at 1:25 PM

Done!!

Sorry, I haven't noticed the "URL_Segment" field because in my own application the name has changed to 'Name' :-)

Thank you anyway

Jul 12, 2012 at 4:17 PM
Edited Jul 12, 2012 at 4:19 PM

I take it that the above method for GetPathDependantOnBrowserLanguage only works for WebForms and not for MVC?

I get The resource cannot be found.

for url - /Default.aspx

 What can i do for MVC?

I need the home page to detect browser language and redirect or respond with the correct language if there is one?

Coordinator
Jul 12, 2012 at 10:20 PM

Maybe you could do something like SelectLanguage on the controller responding to /:

https://github.com/n2cms/n2cms/blob/master/src/Mvc/Dinamico/Dinamico/Defaults.cs