Handling multi lingual properties

229 views
Skip to first unread message

Dody Gunawinata

unread,
May 18, 2011, 4:19:48 AM5/18/11
to ravendb
I am currently puzzling out on how to handle document design that must
support multiple language in some of its property, e.g.

public class Item{
public string Id { get; set;}
public string Name { get; set; }
public string Description { get; set;}
}


Approach 1 - use dictionary
public class Item{
public string Id { get; set;}
public string Name { get; set; }
public Dictionary<string,string> Description { get; set;} // language is key
}

Approach 2 - use explicit property
public class Item{
public string Id { get; set;}
public string Name { get; set; }
public string Description_DE { get; set;}
public string Description_RU { get; set;}
public string Description_EN { get; set;}
}

Approach 3 - one document per language
public class Item{
public string Id { get; set;}
public string Language { get; set;}
public string Name { get; set; }
public string Description { get; set;}
}

Approach 4 - language sub properties
public class Item{
public string Id { get; set;}
public string Name { get; set; }
public ItemLanguage[] LanguageProperties { get; set; }
}

public class ItemLanguage{
public string Language { get; set;}
public string Description { get; set;}
}

Approach 5 - Id hacking
"/Item/123/en";
"/Item/123/de";
"/Item/123/ru";


Any other suggestions? I was thinking that maybe some DB specific
feature like Document Versioning can be used to handle this kind of
stuff.

--
nomadlife.org

Wayne Douglas

unread,
May 18, 2011, 7:26:37 AM5/18/11
to rav...@googlegroups.com
this is an interesting question - one i know i'm going to need to tackle soon.

my opinion off the bat is for approach #1

--
Cheers,

w://

jalchr

unread,
May 18, 2011, 7:33:53 AM5/18/11
to rav...@googlegroups.com
Actually, that depends on how you author the documents. 
Are there always going to be equivalent documents in other languages , or is each language/document is authored separately ?
Which portions are translated and which not ?

In all cases, I would go with option #3:

1) Its clear.
2) Its scalable to any language number
3) It provides a clean way to query/search documents with specific language.


Wayne Douglas

unread,
May 18, 2011, 7:37:28 AM5/18/11
to rav...@googlegroups.com
what about lookups?

woudl you do the same there?

--
Cheers,

w://

Dody Gunawinata

unread,
May 18, 2011, 8:06:00 AM5/18/11
to rav...@googlegroups.com
The problem with approach #3 is that it complicates things like
counting or referencing if you need to have one authoritative
item/information, e.g. I have a car, describe it in 5 different
languages.

--
nomadlife.org

jalchr

unread,
May 18, 2011, 8:06:34 AM5/18/11
to rav...@googlegroups.com
That depends on how you model the lookups ... both options are valid here ... 

If all lookups stored as 1 document, then you need to use option #1 i.e a dictionary with key/text for each language.

If each lookup entry is stored in a separate document, then you would go with option# 3 (still my preference too)

jalchr

unread,
May 18, 2011, 8:11:43 AM5/18/11
to rav...@googlegroups.com, do...@nomadlife.org
I don't see those complications ... can you elaborate ?

Think about your example as in same as 5 posts authored by the same person ... The content is irrelevant whether its in same language or not, but the design is the same !
You can replicate the same Author info across 5 posts, or you can use denoramlized references (like relational world) with 1 Author info and 5 posts related to that document 


Dody Gunawinata

unread,
May 18, 2011, 8:22:26 AM5/18/11
to rav...@googlegroups.com
Say that you are selling a car,

Name : Ferrari Testarossa
General Description : The Ferrari Testarossa is a 12-cylinder
mid-engine sports car manufactured by Ferrari, which went into
production in 1984 as the successor to the Ferrari Berlinetta Boxer. T
Engine : The Testarossa sports a 4.9 litre (4,943 cubic centimetres /
302 cubic inches) Ferrari Colombo flat-12 engine mid mounted.[4][9]
Each cylinder has four valves, with forty-eight valves total,
lubricated via a dry sump system, and a compression ratio of
9.20:1.[4][9] These combine to provide a maximum torque of 490 newton
metres (361 ft·lbf) at 4500 rpm and a maximum power of 291 kilowatts
(396 PS; 390 hp) at 6300 rpm.[1][4][10] Early U.S. versions of the car
had the same engine, but slightly less power with only 283 kW (385 PS;
380 hp).[2][7][10]
Price : $181,000
Code: FT-001

The general description and engine section need to be translated to
multiple languages (Arabic, Italian, Inuit).

As the sales person, I know I only have one car that I have to
describe in multiple languages. Approach #3 will result in 3
documents. When I pull up a list of cars I have in my garage, I have
to filter by language so only one version of the car shows up.

--
nomadlife.org

Ayende Rahien

unread,
May 18, 2011, 8:29:22 AM5/18/11
to rav...@googlegroups.com
I would go with collections for the properties, something like:

public class Item{
 public string Id { get; set;}
 public TranslatedString Name { get; set; }
 public TranslatedString Description { get; set; }
}

public class TranslatedString : Dictionary{
 
 public const string DefaultLang = "en";
 
 public string Value { get { return this[DefaultLang]; }
 
}

Similar to the dictionary approach, but with nicer syntax.

Dody Gunawinata

unread,
May 26, 2011, 8:50:47 AM5/26/11
to rav...@googlegroups.com
I end up with using the following implementation for multi lingual
string (used in asp.net mvc app) - it works quite nicely for my
purposes in handling data insert/edit and automatic display of text
based on current active culture (with invariant text support added)

public class TranslatedString : ListDictionary
{
public const string INVARIANT = "_";
public const string INVARIANT_LABEL = "Neutral";

string _lang;
/// <summary>
/// Initializes a new instance of the <see
cref="T:TranslatedString"/> class.
/// </summary>
public TranslatedString()
{
}


[JsonIgnore]
public string CurrentLanguage
{
get
{
return _lang as string;
}
set
{
_lang = value;
}
}

[JsonIgnore]
public string Value
{
get
{
_lang = _lang ?? Env.GetCurrentCultureCode();
var text = this[_lang] as string;

//If a language specific content is not found, try the
invariant version
if (text.IsNullOrWhiteSpace())
return this[INVARIANT] as string;
else
return text;
}
set
{
_lang = _lang ?? Env.GetCurrentCultureCode();
if (this.Contains(_lang))
this[_lang] = value;
else
Add(_lang, value);
}
}

/// <summary>
/// Only works if there's only one entry in the translated
string. Applicable mainly to be used in model binding.
/// </summary>
/// <param name="lang"></param>
public void ForceLanguage(string lang)
{
if (!this.Contains(lang) && this.Keys.Count == 1)
{
object oldValue = null;
object oldKey = null;
foreach (var k in this.Keys)
{
oldValue = this[k];
oldKey = k;
}

this.Remove(oldKey);

this[lang] = oldValue;
}
}

public void Merge(TranslatedString source)
{
foreach (var v in source.Keys)
this[v] = source[v];
}

/// <summary>
/// Set invariant value
/// </summary>
/// <param name="text"></param>
public void SetInvariant(string text)
{
this[INVARIANT] = text;
}

public static void SetLanguageOnStrings(string lang, bool
isForce, params TranslatedString[] strings)
{
if (isForce)
{
foreach (var s in strings)
s.ForceLanguage(lang);
}
else
{
foreach (var s in strings)
s.CurrentLanguage = lang;
}
}

/// <summary>
/// Mark all the languages with the given text in a target
translated string
/// </summary>
/// <param name="languages"></param>
/// <param name="text"></param>
/// <param name="targetString"></param>
public static void Reset(string[] languages, string text,
TranslatedString targetString)
{
foreach (var l in languages)
{
targetString[l] = text;
}
}
}

--
nomadlife.org

jalchr

unread,
May 26, 2011, 9:09:55 AM5/26/11
to rav...@googlegroups.com, do...@nomadlife.org
Dody, that's interesting ... 
Can you post how you use the above class, just to be complete ?

Dody Gunawinata

unread,
May 26, 2011, 10:48:39 AM5/26/11
to rav...@googlegroups.com
Here's the main idea.

Use TranslatedString in properties you want to support

public class Fragment {


public string Id { get ; set; }

public TranslatedString Content { get; set; }


public string Name { get; set;}
}

This will be stored in RavenDB in this form
{
"Name": "screen-1",
"Content": {
"_": "<img src=\"/file/templates/screen1.png\" />",
"ar": "<img src=\"/file/templates/screen1.png\" /> ",
"en": "<img src=\"/file/templates/screen1.png\" />",
"fr": "<img src=\"/file/templates/screen1.png\" /> ",
"es": "<img src=\"/file/templates/screen1.png\" />"
}
}

In your HTML form, you use

Model.Name.Value e.g. @Html.EditorFor(model => model.Content.Value)
instead of model.Content

[HttpPost]
Create (Fragment f, string lang = "_")
{
//"_" signify invariant
f.SetLanguage(lang, force: true);
Save(f);
}
Note:
You have to use SetLanguage with force to ensure that the data from
the form is set according to the lang parameter you set otherwise it
will default to your current culture. If you don't do this, you will
end up saving "Poyo" in "en" culture instead of "se".

[HttpGet]
Edit(string id, string lang = "_")
{
var fragment = Load(id);
fragment.SetLanguage(lang);
return View(fragment)
}
Note:
You have to use SetLanguage so your Content property display the data
per lang parameter. Remember you have multiple language version for
the Content property but you can only display one in your form. So you
must pick which version of the language you display on your form.
Otherwise it will default to your current environment. This way, you
simply pass /edit/xxx?lang=en or /edit/xx?lang=ar to get English or
Arabic data.

For edit post, it's a bit trickier
[HttpPost]
Edit(string id, Fragment f, string lang = "_")
{
var existingFragment = Load(Id);
//Force form fragment to specific language
f.SetLanguage(lang, force: true);

//Start copy stuff from the form to the existing fragment
existingFragment.Name = f.Name
existingFragment.Content.Merge(f.Content);

//As you can see above, we have to use merge instead of direct
assignment. Remember your form Fragment only contains one language
version. Your existing data might already contains multiple version.
Direct assignment would erase old values

Save(existingFragment)
}

I hope this helps.


On Thu, May 26, 2011 at 4:09 PM, jalchr <jal...@gmail.com> wrote:
> Dody, that's interesting ...
> Can you post how you use the above class, just to be complete ?

--
nomadlife.org

Dody Gunawinata

unread,
May 26, 2011, 11:04:20 AM5/26/11
to rav...@googlegroups.com
This is a more correct Fragment class for previous explanation.

public class Fragment {
public string Id { get ; set; }
public TranslatedString Content { get; set; }
public string Name { get; set;}

public void SetLanguage(string lang, bool force = false)
{
TranslatedString.SetLanguageOnStrings(lang, force, Content);
}
}

--
nomadlife.org

jalchr

unread,
May 26, 2011, 11:13:15 AM5/26/11
to rav...@googlegroups.com, do...@nomadlife.org
Thanks ... much appreciated 

João Bragança

unread,
May 26, 2011, 1:14:10 PM5/26/11
to ravendb
You could get even crazier by leveraging the Accept-Language header
and using a bundle for this. Hmmmmm....

On May 26, 8:13 am, jalchr <jal...@gmail.com> wrote:
> Thanks ... much appreciated
Reply all
Reply to author
Forward
0 new messages