I'm currently working on a site (ASP MVC) that support Multilanguage where every web content is stored in Database, for example I have english version, spanish version, etc. But I'm worried about the performance when it goes to production because everytime a user hit my page, there are at least 20 database calls to get the content.
Is there anything I can do to make this more efficient?
What I can't do is :
Merge all database call into 1 call on every page load (this takes
too much effort).
Make a separate html page for every language (I need the end user to be able to add new language without me changing the code hence the
database design).
I was thinking about caching everything in user's browser and compare it everytime they hit my page and it will only call the database if it doesn't match but I'm not sure how to approach this.
Any help will be appreciated and sorry for bad english.
I would suggest to go with static dictionary in this case, as #Vsevolod-Goloviznin suggested in the comments.
Let me elaborate my approach
You probably have a localized resource in your database identified with some named key and language key as well:
var homePageTitle = Database.GetResource("homeTitle", "es");
You should build up a static dictionary that will be used as cache and will have all the resources in the database in memory, for easy and convenient access:
public static MultiLanguageManager {
private static Dictionary<string, Dictionary<string, string>> ContentCache;
...
public static GetResource(string key, string language) {
return (ContentCache[language])[key];
}
}
Then in your front-end you will have something like this:
...
<title>#MultiLanguageManager.GetResource("aboutTitle", "en")</title>
...
Whenever user changes the localized content in the database, you should rebuild the ContentCache dictionary manually:
// update localized content
...
MultiLanguageManager.RebuildContentCache();
...
By using this approach you can reduce the number of database calls to minimum (retrieve only logic units, and not static resources), and control the consistency of your localization data.
One possible drawback might be the size of the static dictionary that you will build, but with Today's abundance of resources, this should not be a problem in 99% of the cases.
Related
I have made a simple localization of messages. All messages are stored in the static class Lng
public static partial class Lng
{
public static readonly string AppName = "My application";
public static class Category1
{
public static readonly string ConfirmDelete = "Are you sure want to delete?";
}
}
In code usage is as simple as referencing fields
MessageBox.Show(Lng.Category1.ConfirmDelete, ...
Then there is a manager, which does following:
language selection
load corresponding translation
updating fields via reflection
export currently selected language on application exit for an update (in case if default language is selected - to create first translation for any other language)
It's irrelevant of how language files looks likes, but here is a reflection part
TranslateLng("Lng.", typeof(Lng));
...
private static void TranslateLng(string parent, Type type)
{
foreach (Type nested in type.GetNestedTypes())
{
string child = string.Format("{0}{1}.", parent, nested.Name);
TranslateLng(child, nested);
foreach (var field in nested.GetFields())
{
string key = child + field.Name;
DefaultAdd(key, (string)field.GetValue(null)); // store value in default language dictionary (if not created yet)
field.SetValue(null, GetValue(key)); // get value for currently selected language
}
}
This system has one problem: all messages are defined in one class, which required manual management (deleting and updating messages when updating code which uses them).
And I was thinking to change manager to register strings dynamically and simplify usage to something like
MessageBox.Show(Lng.Text("Are you sure want to delete?"), ...
So that text is defined right where it used, duplicated text can be handled by manager and so on.
There are however 2 problems:
I will need a complete list of all messages at the end of application run to export complete list of messages (for currently selected language). What if some of Lng.Text() are never called at that run? Is there a way to register them as they are used in code (compile time?)? So that all calls will be registered somehow, even if peace of code is never used.
How to generate key. I could use CallerMemberName, but right key are more useful, as they are telling exact purpose. To example, Lng.Configuration.Appearance.CaptionText. I could call Lng.Text(key, message), but then I have to manage keys, ensure in their uniqueness, which doesn't appeals me.
I recently worked on a project with internationaliztion and we used Resources in con junction with the Sisulizer program with great success. Having the resources solves your key problem as you manually enter the key when you extract the resources. You also get great support from Resharper which makes the whole process a breeze.
Sisulizer is then used to extract resources as well as strings hard-coded in our Win Forms and WPF classes. It can export a CSV which you can give your translators and it also supports pseudo translation, which makes testing such apps very easy as well.
Background
Our site is a press site and is viewed by many people around the globe. Obviously this means that we will be localising the site in as many languages as possible. A professional translator will be hired for each language.
How our site works currently
The way we have planned to do this is by storing a translation for each element of the page in the database linked by a Guid. So when the page loads the strings are pulled out of the database using the Guid and the language preferences of the user.
We have several documents in our project that contain english translations. Such as this:
public class StandardButtonToken : LocalisationToken
{
protected StandardButtonToken(string defaultText, Guid key) : base(defaultText, key)
{
}
public static readonly LocalisationToken Update = new StandardButtonToken("Update", Guid.Parse("8a999f5b-7ca1-466d-93ca-377321e6de00"));
public static readonly LocalisationToken Go = new StandardButtonToken("Go", Guid.Parse("7a013ecc-0772-4f87-9f1f-da6a878a3c99"));
public static readonly LocalisationToken Edit = new StandardButtonToken("Edit", Guid.Parse("c31be427-5016-475d-997a-96fa5ff8b51f"));
public static readonly LocalisationToken New = new StandardButtonToken("New", Guid.Parse("f72d365c-b18f-4f01-a6e4-b0cd930dc730"));
public static readonly LocalisationToken More = new StandardButtonToken("More", Guid.Parse("bd4da7df-afd2-481e-b6b6-b4a989324758"));
public static readonly LocalisationToken Delete = new StandardButtonToken("Delete", Guid.Parse("ab00ec14-4414-4cda-a8e2-4f03c9e7c5a8"));
public static readonly LocalisationToken Add = new StandardButtonToken("Add", Guid.Parse("01e44600-a556-4a07-8a2a-e69a1ea79629"));
public static readonly LocalisationToken Confirm = new StandardButtonToken("Confirm", Guid.Parse("4c50e91e-3e2f-42fa-97aa-9f1f6f077f09"));
public static readonly LocalisationToken Send = new StandardButtonToken("Send", Guid.Parse("24121766-f424-4d73-ac58-76f90d58b95c"));
public static readonly LocalisationToken Continue = new StandardButtonToken("Continue", Guid.Parse("dd2ca0e5-8a35-4128-b2e8-db68a64a6fe5"));
public static readonly LocalisationToken OK = new StandardButtonToken("OK", Guid.Parse("9a359f93-7c23-44ad-b863-e53c5eadce90"));
public static readonly LocalisationToken Accept = new StandardButtonToken("Accept", Guid.Parse("3206a76b-1cd7-4dc3-9fff-61dfb0992c75"));
public static readonly LocalisationToken Reject = new StandardButtonToken("Reject", Guid.Parse("f99c6a9c-6a55-4f55-ac4b-9581e56d18d3"));
public static readonly LocalisationToken RequestMoreInfo = new StandardButtonToken("Request more info", Guid.Parse("19f3d76b-dafa-47ae-8416-b7d61546b03d"));
public static readonly LocalisationToken Cancel = new StandardButtonToken("Cancel", Guid.Parse("75617287-5418-466b-9373-cc36f8298859"));
public static readonly LocalisationToken Publish = new StandardButtonToken("Publish", Guid.Parse("efd87fd4-e7f1-4071-9d26-a622320c366b"));
public static readonly LocalisationToken Remove = new StandardButtonToken("Remove", Guid.Parse("f7db5d81-5af8-42bf-990f-778df609948e"));
}
Everytime we create a page we make sure the buttons use these Tokens instead of manually writing the text. So if we decide we need a new button we will add a new token in the file below and when the site is run for the first time it will check if it exists in the database and if not it will be created.
So when it comes to the translating we will send these tokens off to the translators and they will change the text only. This will then be added into the site in the relevant language and the page will call the correct translation dependant on the language selected.
Problem/Question
Our translation tokens have the default text as strings, but I'm concerned that the server has to load all of these text strings into memory at start up. They're actually only ever used to store the translation in the db and never otherwise used in code, so I believe it might be a bit wasteful. Instead I believe we could call these translations separately when required, perhaps from some kind of look-up table that's not in-memory.
So the question is. Is the way we are currently doing this going to cause performance issues?
If so can anyone suggest any better solutions?
In total there are 1000's of these tokens in our site.
Google has not been very helpful to me on this occasion so any advice would be much appreciated. I have tried as hard as i can to word this so that it makes sense. Its not an easy one to explain.
Thanks in advance for any help people can provide.
I'm currently working with a team on a project in which we had a similar situation. Our project is a client/server based application. The database with our translations rests on the server and all our controls have default English values. Originally when any new window was opened we read the database and pulled any necessary translations down and translated the controls. This way only the values we needed translated where in memory and only as long as that window needed them. When the window was closed the memory would be reclaimed using garbage collection.
What we discovered was even with all the translations for every button, label, column header, etc... for the entire system was loaded into memory we where only dealing with a couple hundred K. In the end what we did was:
the user logs on:
if they default to another language besides English a data table is set up in memory and populated with the translations from all user controls in our system from the server.
This data table is given a primary key to help improve queries against it. As long as the app is open this data table exists.
Any time a user control is instantiated a method runs that looks up the translation and applies it.
The time it took to log in the user did increase but only by a few milliseconds on average.
Having said all this I wonder if you could use a similar design. This idea would keep the server from having to track translations per session. It would just feed the page to the user.
User logs in:
a datatable object is populated with all the translations for the site
WriteXML() is called
the resulting XML is sent to the client
When the user navigates to a new page:
a client side script is called that uses the XML to translate the page on the client machine.
You could add some caching so that the XML is only replaced when your site is updated or you could just have each user download the XML every time at log on. Either way it takes the load off of the server.
Ignoring other implementations and focusing on the setup you're using, the answer is "yes" and "no".
Yes, there can be performance issues with the method you're following if the number of incoming requests grows and especially as the number of tokens increases. This relationship can be easily explained as "the more requests that come in, the more work the server has to do. the more tokens that exist, the more work the server has to do. If both requests and tokens increase, you're exploding the amount of work the server has to do."
The "no" could be accomplished by adding on to your current solution. You can add a cache to your setup that will save translated pages so that the server would not need to query the database or translate each page per-request. For instance, if you have one page mypage.aspx that contains 20 translatable tokens - let the server translate it the first time and then save the translated file as, say, /localized/mypage.EN.html and all future requests (if the original page hasn't been modified or new tokens haven't been added) will just be sent the cached page instead of re-translating each time.
Additionally, you could have the server generate all translations when the page is updated or tokens are updated instead of waiting for a client-request to come in.
I have a SQL-table with three columns: Id, English and Norwegian. Id is the primary key. In my application I have a flag (EN/NO) to decide which language to use for labels, buttons ++ in the GUI.
The application is now doing a select * everytime the application loads, and the application is looking up all required values at runtime. But instead of loading the whole dataset for every instance, i want to export these values and create a dll so i can store these values locally.
Is there any possibility of creating this in-code so the dll will renew itself with every build? Or do I have to run some external program to dynamically create ex. a .cs code to copy/paste into my class? (I need to be able to re-run the process because rows will be added every time there is a need for a new label/text)
I have so far thought out three solutions on how to structure my export, but no clue on how to export the data:
Preserve the state of the DataTable in a static context and provide help-methods to standardize the way of getting the values out.
Create a class containing each unique ID as method-name, and a parameter to decide which value to return:
public static class Names
{
public static string 12345(string language)
{
switch (language)
{
case "EN":
return "Hello";
case "NO":
return "Hei";
default:
return "Hello";
}
}
}
Create a class containing a searchable list for each language with ID as key and the value (as value)
Why don't you create different resource files for different languages and load the appropriate one depending you the settings. You can do this by using System.Resources.ResourceManager. This article here explains this in detail.
EDIT: Following SO post also discuss this in detail Best practice to make a multi language application in C#/WinForms?
No, i don't like the idea to put internationalization strings into a class library, Why you don't just use the .NET internationalization feature already built in in the framework ?
Resource files are the best solution, not class library for this kind of work ...
What can I do to avoid the repeated loading of static data every time a page is loaded in ASP.NET MVC2? This is pretty wasteful, and I would like to fix it.
My master page has the typical User Name + Company Name display in the top right corner. But because of the stateless nature of MVC, this data has to be looked up every single time a page is loaded, even though it never changes.
In the old webforms days I would just throw it into Session, but that seems to be discouraged in MVC. Plus the web app runs on a webfarm, so I really do not want to use session.
The web app already has quite a few static data items that get queried on every page load (user currency, user tax rate, etc), so I think a performance gain can be made by loading them only once at login time. I'm just not sure what the correct MVC method is (I am still quite new to MVC).
I always use the built-in ASP.NET cache for this, set the expiry appropriately and you're good to go.
public void Blah()
{
var company = HttpRuntime.Cache.Get("Company") as string;
if (company == null)
{
company = FetchCompanyFromDb();
HttpRuntime.Cache["Company"] = company;
}
return this.View(company);
}
Also see this previous question:
Caching in asp.net-mvc
Could you maybe encapsulate your User currency / User name / Tax etc into a user control and then use Output Caching on that?
I'm wondering how others deal with trying to centralize MessageBox function calling. Instead of having long text embedded all over the place in code, in the past (non .net language), I would put system and application base "messagebox" type of messages into a database file which would be "burned" into the executable, much like a resource file in .Net. When a prompting condition would arise, I would just do call something like
MBAnswer = MyApplication.CallMsgBox( IDUserCantDoThat )
then check the MBAnswer upon return, such as a yes/no/cancel or whatever.
In the database table, I would have things like what the messagebox title would be, the buttons that would be shown, the actual message, a special flag that automatically tacked on a subsequent standard comment like "Please contact help desk if this happens.". The function would call the messagebox with all applicable settings and just return back the answer. The big benefits of this was, one location to have all the "context" of messages, and via constants, easier to read what message was going to be presented to the user.
Does anyone have a similar system in .Net to do a similar approach, or is this just a bad idea in the .Net environment.
We used to handle centralized messages with Modules (VB). We had one module with all messages and we call that in our code. This was done so that we change the message in one place (due to business needs) and it gets reflected everywhere. And it was also easy to handle change in one file instead of multiple files to change the message. Also we opened up that file to Business Analysts (VSS) so that they can change it. I don't think it is a bad idea if it involves modules or static class but it might be a overkill to fetch it from DB.
HTH
You could use resource files to export all text into there (kinda localization feature as well). Resharper 5.0 really helps in that highlighting text that can be moved to resource.
Usually it looks like this:
Before: MessageBox.Show(error.ToString(), "Error with extraction");
Suggestion: Localizable string "Error with extraction"
Right click Move to Resource
Choose resource file and name (MainForm_ExtractArchive_Error_with_extraction), also check checkbox Find identical items in class ...
Call it like this MessageBox.Show(error.ToString(), Resources.MainForm_ExtractArchive_Error_with_extraction);
Best of all it makes it easy to translate stuff to other languages as well as keeping text for MessageBox in separate Resource. Of course Resharper does it all for you so no need to type that much :-)
I suppose you could use a HashTable to do something similar like this, this can be found in:
using System.Collections;
To keep it globally accessable i was thinking a couple of functions in a class holding the hashtable to get/set a certain one.
lets see now.
public class MessageBoxStore
{
private HashTable stock;
public string Get(string msg)
{
if (stock.ContainsKey(msg))
return stock[msg];
else
return string.Empty;
}
public string Set(string msg, string msgcontent)
{
stock[msg] = msgcontent;
}
}
or something like that, you could keep multiple different information in the hashtable and subsequently compose the messagebox in the function too.. instead of just returning the string for the messagebox contents...
but to use this it would be quite simple.
call a function like this on program load.
public LoadErrorMessages()
{
storeClass = new MessageBoxStore();
storeClass.Set("UserCantDoThat", "Invalid action. Please confirm your action and try again");
}
for example, and then.
MessageBox.Show(storeClass.Get("UserCantDoThat"));
i put this in a new class instead of using the HashTable get/set methods direct because this leaves room for customization so the messagebox could be created in the get, and more than 1 piece of information could be stored in the set to handle messagebox title, buttontype, content, etc etc.