Visualforce Component for Locale Based DateTime Display

Sometimes, I really dislike Visualforce... Last week I spent a ton of time troubleshooting a display issue for a page that was built by another developer. The issue was that the DateTime field for an object was not displaying properly. It was showing the value in GMT and not the timezone of the user. After spending way too much time reviewing everything I decided to build a custom Visualforce component to eliminate the problem altogether.

Upon searching the developer boards and Google I found a bunch of people were having similar issues but the responses being posted to the other queries were less than helpful/useful. I tried to search for a number of different topics but in general I was looking to see what others were doing to properly display a datetime value in a Visualforce page based upon the viewing user's timezone and locale. Because both of these settings are very important if you are building pages that will be displayed to users across timezones and country borders.

In my opinion the question at hand was seemingly simple enough and I was hoping that my search would result in some type of "doh!" moment when I would read a post and go "that sounds right" or something like that, which is something that often happens to me.

Anyway, I found a number of responses where people simply copied the standard help documentation for displaying formatted datetime values in Visualforce via the "outputText" tag. This response is as follows:

<apex:outputText value="The formatted time right now is: 
	{0,date,yyyy.MM.dd G 'at' HH:mm:ss z}">
	<apex:param value="{!NOW()}" />

So it's one thing to display a datetime value for a system variable like NOW() but what about displaying a datetime value that is stored on a record. Using the code above simply renders a formatted datetime value in GMT. Not only that but if you need to make the page display for more than one locale then you need to account for that in the page or whatever.

Since the developer boards and miscellaneous searches on Google were not assisting me I decided that I should simply try some variations and see what works. After much trial and error I found that if you use an "outputText" tag in the page you can get the locale and timezone issue addressed but only if you include some other characters within the "value" attribute of the tag. More specifically, if you add this code to your page "<apex:outputText value="{!Account.LastModifiedDate}"></apex:outputText>" the resulting text on the rendered page is "Sat Feb 5 20:52:56 GMT 2011." However, if you do this "<apex:outputText value=", {!Account.LastModifiedDate}"></apex:outputText>" then you get the properly formatted locale and timezone value of ", 2/5/2011 1:52 PM," which is correct for me because my user record is setup in the Denver timezone and the United States locale. The problem is that now I have this miscellaneous character displayed in the page, which I may not want.

Furthermore, I guess I just don't understand the reason(s) why this subtle variation in the code will cause what seems to be a significant difference in display. I suppose someone reading this may know the reason(s) but I've learned that sometimes it's better to simply move on.

Well let's get into the code for my solution then. The resulting code set required a couple of things. First, I created a Visualforce component that should receive a datetime value from a Visualforce page and return a properly formatted datetime value.

<apex:component access="global" controller="controller_locale_formatted_datetime">
	Created by: Greg Hacic
	Last Update: 4 February 2011 by Greg Hacic
	Copyright (c) 2011 Interactive Ties LLC
			- when adding to your Salesforce org set the name of component to locale_formatted_datetime
<apex:attribute assignTo="{!date_time}" description="The DateTime value to be rendered based upon the user's locale" name="date_time_value" type="DateTime"></apex:attribute>

Second, I created the controller to do the actual work of determining the locale of the running user so that the code will result in the proper format.

	Created by: Greg Hacic
	Last Update: 4 February 2011 by Greg Hacic
	Copyright (c) 2011 Interactive Ties LLC
			- controller for the locale_formatted_datetime Visualforce component
public class controller_locale_formatted_datetime {
	public DateTime date_time { get; set; } //property that reads the datetime value from the component attribute tag
	//returns the properly formatted datetime value
	public String getTimeZoneValue() {
		Map<String, String> mappedValues = new Map<String, String>(); //map for holding locale to datetime format
		mappedValues = MapValues(); //populate the map with all the locale specific datetime formats
		String user_locale = UserInfo.getLocale(); //grab the locale of the user
		String datetime_format = 'M/d/yyyy h:mm a'; //variable for the datetime format defaulted to the US format
		if (mappedValues.containsKey(user_locale)) { //if the map contains the correct datetime format
			datetime_format = mappedValues.get(user_locale); //grab the datetime format for the locale
		String locale_formatted_date_time_value = date_time.format(datetime_format); //create a string with the proper format
		return locale_formatted_date_time_value; //return the string
	//populate a map with locale values and corresponding datetime formats
	private Map<String, String> MapValues() {
		Map<String, String> locale_map = new Map<String, String>(); //holds the locale to timedate formats
		locale_map.put('ar', 'dd/MM/yyyy hh:mm a');
		locale_map.put('ar_AE', 'dd/MM/yyyy hh:mm a');
		locale_map.put('ar_BH', 'dd/MM/yyyy hh:mm a');
		locale_map.put('ar_JO', 'dd/MM/yyyy hh:mm a');
		locale_map.put('ar_KW', 'dd/MM/yyyy hh:mm a');
		locale_map.put('ar_LB', 'dd/MM/yyyy hh:mm a');
		locale_map.put('ar_SA', 'dd/MM/yyyy hh:mm a');
		locale_map.put('bg_BG', 'yyyy-M-d H:mm');
		locale_map.put('ca', 'dd/MM/yyyy HH:mm');
		locale_map.put('ca_ES', 'dd/MM/yyyy HH:mm');
		locale_map.put('ca_ES_EURO', 'dd/MM/yyyy HH:mm');
		locale_map.put('cs', 'd.M.yyyy H:mm');
		locale_map.put('cs_CZ', 'd.M.yyyy H:mm');
		locale_map.put('da', 'dd-MM-yyyy HH:mm');
		locale_map.put('da_DK', 'dd-MM-yyyy HH:mm');
		locale_map.put('de', 'dd.MM.yyyy HH:mm');
		locale_map.put('de_AT', 'dd.MM.yyyy HH:mm');
		locale_map.put('de_AT_EURO', 'dd.MM.yyyy HH:mm');
		locale_map.put('de_CH', 'dd.MM.yyyy HH:mm');
		locale_map.put('de_DE', 'dd.MM.yyyy HH:mm');
		locale_map.put('de_DE_EURO', 'dd.MM.yyyy HH:mm');
		locale_map.put('de_LU', 'dd.MM.yyyy HH:mm');
		locale_map.put('de_LU_EURO', 'dd.MM.yyyy HH:mm');
		locale_map.put('el_GR', 'd/M/yyyy h:mm a');
		locale_map.put('en_AU', 'd/MM/yyyy HH:mm');
		locale_map.put('en_B', 'M/d/yyyy h:mm a');
		locale_map.put('en_BM', 'M/d/yyyy h:mm a');
		locale_map.put('en_CA', 'dd/MM/yyyy h:mm a');
		locale_map.put('en_GB', 'dd/MM/yyyy HH:mm');
		locale_map.put('en_GH', 'M/d/yyyy h:mm a');
		locale_map.put('en_ID', 'M/d/yyyy h:mm a');
		locale_map.put('en_IE', 'dd/MM/yyyy HH:mm');
		locale_map.put('en_IE_EURO', 'dd/MM/yyyy HH:mm');
		locale_map.put('en_NZ', 'd/MM/yyyy HH:mm');
		locale_map.put('en_SG', 'M/d/yyyy h:mm a');
		locale_map.put('en_US', 'M/d/yyyy h:mm a');
		locale_map.put('en_ZA', 'yyyy/MM/dd hh:mm a');
		locale_map.put('es', 'd/MM/yyyy H:mm');
		locale_map.put('es_AR', 'dd/MM/yyyy HH:mm');
		locale_map.put('es_BO', 'dd-MM-yyyy hh:mm a');
		locale_map.put('es_CL', 'dd-MM-yyyy hh:mm a');
		locale_map.put('es_CO', 'd/MM/yyyy hh:mm a');
		locale_map.put('es_CR', 'dd/MM/yyyy hh:mm a');
		locale_map.put('es_EC', 'dd/MM/yyyy hh:mm a');
		locale_map.put('es_ES', 'd/MM/yyyy H:mm');
		locale_map.put('es_ES_EURO', 'd/MM/yyyy H:mm');
		locale_map.put('es_GT', 'd/MM/yyyy hh:mm a');
		locale_map.put('es_HN', 'MM-dd-yyyy hh:mm a');
		locale_map.put('es_MX', 'd/MM/yyyy hh:mm a');
		locale_map.put('es_PE', 'dd/MM/yyyy hh:mm a');
		locale_map.put('es_PR', 'MM-dd-yyyy hh:mm a');
		locale_map.put('es_PY', 'dd/MM/yyyy hh:mm a');
		locale_map.put('es_SV', 'MM-dd-yyyy hh:mm a');
		locale_map.put('es_UY', 'dd/MM/yyyy hh:mm a');
		locale_map.put('es_VE', 'dd/MM/yyyy hh:mm a');
		locale_map.put('et_EE', 'd.MM.yyyy H:mm');
		locale_map.put('fi', 'd.M.yyyy H:mm');
		locale_map.put('fi_FI', 'd.M.yyyy H:mm');
		locale_map.put('fi_FI_EURO', 'd.M.yyyy H:mm');
		locale_map.put('fr', 'dd/MM/yyyy HH:mm');
		locale_map.put('fr_BE', 'd/MM/yyyy H:mm');
		locale_map.put('fr_CA', 'yyyy-MM-dd HH:mm');
		locale_map.put('fr_CH', 'dd.MM.yyyy HH:mm');
		locale_map.put('fr_FR', 'dd/MM/yyyy HH:mm');
		locale_map.put('fr_FR_EURO', 'dd/MM/yyyy HH:mm');
		locale_map.put('fr_LU', 'dd/MM/yyyy HH:mm');
		locale_map.put('fr_MC', 'dd/MM/yyyy HH:mm');
		locale_map.put('hr_HR', 'yyyy.MM.dd HH:mm');
		locale_map.put('hu', 'yyyy.MM.dd. H:mm');
		locale_map.put('hy_AM', 'M/d/yyyy h:mm a');
		locale_map.put('is_IS', 'd.M.yyyy HH:mm');
		locale_map.put('it', 'dd/MM/yyyy');
		locale_map.put('it_CH', 'dd.MM.yyyy HH:mm');
		locale_map.put('it_IT', 'dd/MM/yyyy');
		locale_map.put('iw', 'HH:mm dd/MM/yyyy');
		locale_map.put('iw_IL', 'HH:mm dd/MM/yyyy');
		locale_map.put('ja', 'yyyy/MM/dd H:mm');
		locale_map.put('ja_JP', 'yyyy/MM/dd H:mm');
		locale_map.put('kk_KZ', 'M/d/yyyy h:mm a');
		locale_map.put('km_KH', 'M/d/yyyy h:mm a');
		locale_map.put('ko', 'yyyy. M. d a h:mm');
		locale_map.put('ko_KR', 'yyyy. M. d a h:mm');
		locale_map.put('lt_LT', 'yyyy.M.d');
		locale_map.put('lv_LV', 'yyyy.d.M HH:mm');
		locale_map.put('ms_MY', 'dd/MM/yyyy h:mm a');
		locale_map.put('nl', 'd-M-yyyy H:mm');
		locale_map.put('nl_BE', 'd/MM/yyyy H:mm');
		locale_map.put('nl_NL', 'd-M-yyyy H:mm');
		locale_map.put('nl_SR', 'd-M-yyyy H:mm');
		locale_map.put('no', 'dd.MM.yyyy HH:mm');
		locale_map.put('no_NO', 'dd.MM.yyyy HH:mm');
		locale_map.put('pl', 'yyyy-MM-dd HH:mm');
		locale_map.put('pt', 'dd-MM-yyyy H:mm');
		locale_map.put('pt_AO', 'dd-MM-yyyy H:mm');
		locale_map.put('pt_BR', 'dd/MM/yyyy HH:mm');
		locale_map.put('pt_PT', 'dd-MM-yyyy H:mm');
		locale_map.put('ro_RO', 'dd.MM.yyyy HH:mm');
		locale_map.put('ru', 'dd.MM.yyyy H:mm');
		locale_map.put('sk_SK', 'd.M.yyyy H:mm');
		locale_map.put('sl_SI', 'd.M.y H:mm');
		locale_map.put('sv', 'yyyy-MM-dd HH:mm');
		locale_map.put('sv_SE', 'yyyy-MM-dd HH:mm');
		locale_map.put('th', 'M/d/yyyy h:mm a');
		locale_map.put('th_TH', 'd/M/yyyy, H:mm ?.');
		locale_map.put('tr', 'dd.MM.yyyy HH:mm');
		locale_map.put('ur_PK', 'M/d/yyyy h:mm a');
		locale_map.put('vi_VN', 'HH:mm dd/MM/yyyy');
		locale_map.put('zh', 'yyyy-M-d ah:mm');
		locale_map.put('zh_CN', 'yyyy-M-d ah:mm');
		locale_map.put('zh_HK', 'yyyy-M-d ah:mm');
		locale_map.put('zh_TW', 'yyyy/M/d a h:mm');
		return locale_map; //return the map


If you decide to use this code then I recommend that you take a second to review the map that I created above where I determine the datetime format for each of the various Salesforce locales. I think I have them all in there and I think they are all accurate but they may need to be edited. Feel free to update that map.

I never created a custom controller for a component before. I thought I would have to do a PageReference in there somewhere and I didn't want to create a Visualforce page for a pretty simple component. So I was a bit lost about how to test that class. It took me a few tries to get the test to work as intended. The code below will give you 100% coverage.

	Created by: Greg Hacic
	Last Update: 4 February 2011 by Greg Hacic
	Copyright (c) 2011 Interactive Ties LLC
			- tests the controller_locale_formatted_datetime class
private class test_locale_formatted_datetime {
	static testMethod void test_logic() {
		//test for English (United States) format
		List<User> userUpdate = new List<User>(); //list for holding user updates
		userUpdate.add(new User(Id = UserInfo.getUserId(), LocaleSidKey = 'en_US')); //set the locale for the running user to English (United States)
		update userUpdate; //make the update
		controller_locale_formatted_datetime controller = new controller_locale_formatted_datetime();
		controller.date_time = DateTime.valueOf('2007-01-01 2:35:21'); //set the datetime variable to 1 January 2007
		String test_value = controller.getTimeZoneValue(); //run the logic and format the datetime value
		System.assertEquals('1/1/2007 2:35 AM', test_value); //validate the results
		//now test for Arabic (Saudi Arabia) format
		userUpdate.clear(); //remove all user sobjects from the userUpdate List
		userUpdate.add(new User(Id = UserInfo.getUserId(), LocaleSidKey = 'ar_SA')); //set the locale for the running user to Arabic (Saudi Arabia)
		update userUpdate; //make the update
		controller.date_time = DateTime.valueOf('2005-03-07 5:02:21'); //set the datetime variable to 7 March 2005
		test_value = controller.getTimeZoneValue(); //run the logic and format the datetime value
		System.assertEquals('07/03/2005 05:02 AM', test_value); //validate the results

		//now test for Chinese (Taiwan) format
		userUpdate.clear(); //remove all user sobjects from the userUpdate List
		userUpdate.add(new User(Id = UserInfo.getUserId(), LocaleSidKey = 'zh_TW')); //set the locale for the running user to Chinese (Taiwan)
		update userUpdate; //make the update
		controller.date_time = DateTime.newInstance(2011, 1, 3, 12, 41, 15); //set the datetime variable to 3 January 2011
		test_value = controller.getTimeZoneValue(); //run the logic and format the datetime value
		System.assertEquals('2011/1/3 PM 12:41', test_value); //validate the results

If you need a practical use case for the code then please copy and paste the Visualforce page from below.

<apex:page standardController="Account" tabStyle="Account">
	Created by: Greg Hacic
	Last Update: 5 February 2011 by Greg Hacic
	Copyright (c) 2011 Interactive Ties LLC
<apex:sectionHeader title="Visualforce Example" subtitle="{!Account.Name}"></apex:sectionHeader>
		<apex:pageBlock title="Account Information" mode="detail">
			<apex:pageBlockSection columns="2" title="General Information">
				<apex:outputField value="{!Account.Name}"></apex:outputField>
				<apex:outputField value="{!Account.OwnerId}"></apex:outputField>
				<apex:outputField value="{!Account.Site}"></apex:outputField>
				<apex:outputField value="{!Account.Website}"></apex:outputField>
			<apex:pageBlockSection columns="2" title="Unformatted System Information">
				<apex:outputField value="{!Account.CreatedById}">
					<apex:outputText value=", {0,date,M/d/yyyy h:mm a}">
						<apex:param value="{!Account.CreatedDate}"></apex:param>
				<apex:outputField value="{!Account.LastModifiedById}">, <apex:outputText value="{!Account.LastModifiedDate}"></apex:outputText></apex:outputField>
			<apex:pageBlockSection columns="2" title="Formatted System Information">
				<apex:outputField value="{!Account.CreatedById}">, <c:locale_formatted_datetime date_time_value="{!Account.CreatedDate}"></c:locale_formatted_datetime></apex:outputField>
				<apex:outputField value="{!Account.LastModifiedById}">, <c:locale_formatted_datetime date_time_value="{!Account.LastModifiedDate}"></c:locale_formatted_datetime></apex:outputField>

This works with the standard Account object and the standard Account fields so you can copy and paste without worrying about any edits prior to seeing it all work together.

Automated Exchange Rates in

Reduce Repetitive Tasks, Eliminate Errors & Free Up Your Administrators.

Birthday Reminders for

It might lead to a sale. Or it might make you feel good.