There are various ways to create a data-driven multi-language site, but the method I will show in this article series uses an underused technique involving ColdFusion custom tags. Basically, the technique shown will turn any simple, well-formed HTML or XHTML page into a content management system with only one tag required on the page. This first part of this series showed the principles behind the custom tag technique. This part will implement the database tables and custom ColdFusion functionality to turn HTML tags into database-driven dynamic tags to implement the multi-language aspect. The third part will create a simple administrative interface to administer the system.

Where we are

If you haven't read the first part of the series , it is essential before proceeding. The files created were mastertag.cfm, which will form the basis of the system, and the html tag replacement files -- p.cfm, h1.cfm, h2.cfm, h3.cfm, td.cfm, th.cfm, and span.cfm. You can also add other tags to this as well, such as a.cfm, div.cfm, label.cfm, or any other base HTML tag that you may need to reference. These files are named for the html tags and will each act as a replacement for the actual HTML tag in each page that imports the tag library. The one requirement for this system will be that every tag you want to use will require an ID attribute in the tag, as in the following:

<h1 id="mainheading">This is my heading</h1>
<p id="main">This is my paragraph</p>

This is obviously not a requirement in HTML, however to turn these HTML tags into data-driven custom tags, we need to be able to differentiate between tags on a page.

Note: Although this tutorial talks about multiple languages, you can use the technique for only 1 language if you so desire -- and the the capability is in place for a later translation.

Before we proceed with the functionality, we will need two database tables. You can create these in any database, but the following code will work as is in SQL Server. Basically, we will have one table to hold all site content, and one table to hold the language names:

CREATE TABLE SiteLanguages (
  languageid int IDENTITY (1, 1) NOT NULL ,
  sitelanguage varchar (128) NULL
)
GO

CREATE TABLE PageContent (
  pageContentID int IDENTITY (1, 1) NOT NULL ,
  pageContentLanguage int NULL ,
  pageContentPage varchar (255) NULL ,
  pageContent varchar (4000) NULL ,
  pageContentTagId varchar (255) NULL
)

The first table holds the language name and a primary key for the language. We'll fill it up with English and Spanish:

languageidsitelanguage
1English
2Spanish

The second table needs a little more description. It will hold all content that will occupy all of the tags that we have defined (p, h1, h2, etc). The pageContentID field contains the primary key that will reference our content. The pageContentLanguage field contains a key reference to the language of that content (foreign key to the SiteLanguages table). The pageContentPage field contains the page name. We will store page names using a full path to the page, but replacing slashes, backslashes, and period characters with underscores to make our administration easier. The pageContent field contains all content. Finally the pageContentTagId contains the id attribute of the tag in question.

Basically, each tag content will be stored in the database referenced by page name and by ID. Referencing by ID alone would not work if you have duplicate IDs across pages. Page content at the page level will be pulled from the database and stored in Request scope in a struct using the form:

Request.PageContent[PageName][TagID]

This will make it easy to retrive data from the database based on page name, put it into the struct, and also take into account the fact that you may have include files that you do not want to store more than once. For example, lets say you have a footer.cfm include file that has several links in it. You do not want to store each link separately for each page. That would make content management a nightmare and remove the advantages of includes. We will simply store the include file name in the database rather than the page the include appears on.

Making it work

Before proceeding to show the code, I will point out that one of the cool things about this technique is that you can apply it to an existing site, with a few modifications to your pages to make sure the tags have IDs. The custom tag will actually suck the content out of your pages and insert it into the database upon first view of the page. Upon each succeeding view, the content comes directly from the database. You can also remove the database-driven aspect at any time simply by removing the <cfimport> statement at the top of the page.

First, you will need a function in your application.cfm file so that all pages have access to it in Request scope. Put the following code in application.cfm:

<!--- Set up a function to grab the page name.
we'll use cftry/cfcatch to create an error that reports the page name --->

<cffunction name="getCallingTemplatePath">
 <cfset var callingTemplatePath = ""/>
 <cftry>
  <cfthrow>
  <cfcatch>
   <cfset callingTemplatePath = cfcatch.tagcontext[4].template>
  </cfcatch>
 </cftry>
 <cfreturn callingTemplatePath />
</cffunction>
<cfset request.getCallingTemplatePath = getCallingTemplatePath>

The code shown for basetag.cfm in the last part was the core of the tag, but the following is the completed tag with all database functionality in place. It is commented inline, but I will explain a few things about it at the end.

<cfsilent><cfparam name="tag" default="div">
<cfparam name="thistag.executionmode" default="">
<cfparam name="attributes.id" default="">
<!--- Set up datasource here --->
<cfset request.datasource="testdatabase">
<!--- Set up local directory --->
<cfset request.basepath = "c:\webroot\yoursitefolder">

<!--- Use the function to grab the page name --->
<cfset request.pageName = getCallingTemplatePath()>

<!--- Every tag used must have an ID. A special "ignore" attribute can be used to ignore the tag--->
<cfif not IsDefined("attributes.ignore") or attributes.id EQ "">
  <!--- Set up the page name for the database insert. Change / \ . to _ --->
  <cfparam name="attributes.page" default="#cgi.SCRIPT_NAME#">
  <cfif isdefined("request.pagename")><cfset attributes.page=request.pagename></cfif>
  <cfset attributes.page = Replace(attributes.page, request.basepath, "","all")>
  <cfset attributes.page = Replace(attributes.page, "/","_","all")>
  <cfset attributes.page = Replace(attributes.page, "\","_","all")>
  <cfset attributes.page = Replace(attributes.page, ".cfm","_cfm","all")>
  <cfset attributes.page = Replace(attributes.page, ".","","all")>
  <cfset ignoreTag = false>
<cfelse>
  <cfset ignoreTag = true>
</cfif>

<!--- On start tag, set up attributes for HTML tag --->
</cfsilent><cfswitch expression="#ThisTag.ExecutionMode#">
<cfcase value="start"><cfsilent>
<cfset adminLink = "">
<!--- Insert admin link functionality here in Part 3 --->

<!--- First, create all the original HTML attributes in a list to output them to the page --->
<cfset listOfAttributes = "">
<cfloop collection="#attributes#" item="i">
<cfif i NEQ "page" and i NEQ "ignore">
<cfset listOfAttributes = '#listOfAttributes# #LCase(i)#="#attributes[i]#"'>
</cfif>
</cfloop>
</cfsilent><cfif tag EQ "a"><cfoutput>#adminLink#<#tag##listOfAttributes#></cfoutput>
<cfelse><cfoutput><#tag##listOfAttributes#>#adminLink#</cfoutput></cfif></cfcase>

<!--- On end tag, set up everything else --->
<cfcase value="end">
<cfsilent>
<cfif attributes.id NEQ "">
  <!--- Set up the session language if not set up --->
  <cfif not IsDefined("Session.language")>
    <cfset Session.Language = 1>
  </cfif>
  <!--- Set up the user-default language if not set up --->
  <cfif isdefined("cookie.language") AND cookie.language NEQ session.language>
    <cfset Session.Language = cookie.language>
  </cfif>
  <!--- Set up the new language user clicks link --->
  <cfif isDefined("url.language")>
    <cfquery name="getLanguage" datasource="#request.datasource#">
    SELECT languageid FROM SiteLanguages
    WHERE SiteLanguage = '#url.language#'
    </cfquery>
  <cfif getLanguage.recordcount NEQ 0>
    <cfset Session.Language = getLanguage.languageid>
    <cfset cookie.language = Session.Language>
  </cfif>
</cfif>
<!--- Create new struct to hold all page content --->
<cfparam name="request.pagecontent" default="#StructNew()#">
<!--- Test for existance in database of tag --->
<cfquery name="rsGetTagContents" datasource="#request.datasource#">
SELECT pageContentID FROM pageContent
WHERE pageContentLanguage = '#Session.Language#'
AND pageContentPage = '#attributes.page#'
AND pageContentTagID = '#attributes.id#'
</cfquery>
<!--- If it doesn't exist, insert it and set the struct key --->
<cfif rsGetTagContents.RecordCount EQ 0>
  <cfquery datasource="#request.datasource#">
  INSERT INTO pageContent
  (pageContentLanguage, pageContentPage, pageContentTagId, pageContent)
  VALUES
  ('#session.language#','#attributes.page#','#attributes.id#','#ThisTag.generatedContent#')
  </cfquery>
  <cfset request.PageContent[attributes.page][#attributes.id#] = ThisTag.generatedContent>
</cfif>
<!--- If struct key not exist for page, get all content from Database --->
<cfif NOT StructKeyExists(request.pagecontent, attributes.page)>
  <cfquery name="rsGetText" datasource="#request.datasource#">
  SELECT pageContent, pageContentTagId
  FROM pageContent
  WHERE pageContentLanguage = '#Session.Language#'
  AND pageContentPage = '#attributes.page#'
  </cfquery>
<cfloop query="rsGetText">
  <cfset request.PageContent[attributes.page][#rsGetText.pageContentTagId#] = rsGetText.pageContent></cfloop>
</cfif>

<!--- Change the tag output substituting what is in the tag for what is in the database --->
<cfset thistag.generatedContent = request.PageContent[attributes.page][attributes.id]>
</cfif></cfsilent><cfoutput></#tag#></cfoutput>
</cfcase>
</cfswitch>

The code uses the function from application.cfm to get the page name of the file that the tags reside in. This is hard because there is no way to grab an include file name, however rather than develop an elaborate system of reporting page names and including code in each include file, this function uses the error handling mechanism of ColdFusion to get the name of the included file. Now, when we include files like footer.cfm, menu.cfm, etc, there is only one instance of this file in the database. This makes managing the content much easier, and the function consumes almost zero overhead.

When a page is browsed, each tag that has a corresponding file in the /html/ directory will go to the basetag.cfm. If the tag has no ID attribute or has an attribute of ignore set to true, the tag is simply treated as an html tag -- attributes are output and no database interaction takes place. However, if there is an ID, the basetag page does the following:

  1. Replaces all \ / and . characters in the file path, as well as the hard drive location
  2. Make a list of existing attributes to output to the html page. If we leave out this step, attributes would be stripped from the tag.
  3. Output the start tag with attributes.
  4. Check for a default language in the Session variable named language. If none exists, set it to 1 (the default of English).
  5. If a language is found in a user cookie, use that instead.
  6. If a language is passed in a url, use that and set the cookie and session.
  7. Create a struct if it doesn't already exist to hold all page content.
  8. If the ID does not exist in the database, insert it with the content from the tag.
  9. If this is the first tag on the page, hit the database and grab all content for this page.
  10. Put the content in the struct.
  11. Output the content, followed by the closing tag.

Step 8 is the bullworker here -- it grabs any content that isn't already in the database and puts it there. Once it is there, you can modify it in the database using the admin interface we'll create in Part 3.

Testing

To test out the system so far, we'll use the following very simple page with dummy content. The entire body content of testtags.cfm is as follows:

<div id="top">
<h1>Tom's Too-Cool Design Studio</h1>
</div>
<div id="col1">
<cfinclude template="includes/menu.cfm">
<cfinclude template="includes/banner.cfm">
</div>
<div id="col2">
<h2 id="pagetitle">My Content goes here</h2>
<p id="p2">This tag has nothing interesting in it. Still, it has snazz because it's coming from the database. The following is total gibberish. </p>
<p id="p3">Ten years ago a crack commando unit was sent to prison by a military court for a crime they didn't commit. These men promptly escaped from a maximum security stockade to the Los Angeles underground. Today, still wanted by the government, they survive as soldiers of fortune. If you have a problem and no one else can help, and if you can find them, maybe you can hire the A-team.</p>
<p id="p4">Mutley, you snickering, floppy eared hound. When courage is needed, you're never around. Those medals you wear on your moth-eaten chest should be there for bungling at which you are best. So, stop that pigeon, stop that pigeon, stop that pigeon, stop that pigeon, stop that pigeon, stop that pigeon, stop that pigeon. Howwww! Nab him, jab him, tab him, grab him, stop that pigeon now.</p>
<p id="p5">Ulysses, Ulysses - Soaring through all the galaxies. In search of Earth, flying in to the night. Ulysses, Ulysses - Fighting evil and tyranny, with all his power, and with all of his might. Ulysses - no-one else can do the things you do. Ulysses - like a bolt of thunder from the blue. Ulysses - always fighting all the evil forces bringing peace and justice to all.</p>
</div>
<div id="footer">
<cfinclude template="includes/footer.cfm">
</div>

We'll also use the following includes:

banner.cfm

<cfimport taglib="../html"><table style="padding:10px">
<tr>
<td><h2><a href="http://www.thisisnotareallink.com" id="joes">Eat at Joe's</a></h2></td>
</tr>
<tr>
<td><p id="joesdesc">Fried rats on a stick, pig entrails, crunchy frog, and other assorted delicacies served with fast-food efficiency.</td>
</tr>
<tr>
<td><h3 id="phone">Call 555-555-EATS</h3></td>
</tr>
</table>

footer.cfm

<cfimport taglib="../html"><div id="footer">
<p id="contents">Contents of this site copyright 2006</p>
<p><a href="index.cfm" id="home">Home</a> |
<a href="about.cfm" id="about">About Us</a> |
<a href="faq.cfm" id="faq">FAQ</a> |
<a href="privacy.cfm" id="privacy">Privacy Policy</a> |
<a href="contact.cfm" id="contact">Contact Us</a></p>
</div>

menu.cfm

<cfimport taglib="../html"><ul>
<li><a href="/" id="home">Home</a></li>
<li><a href="/products/" id="products">Products</a></li>
<li><a href="/services/" id="services">Services</a></li>
<li><a href="/contact/" id="contact">Contact</a></li>
<li><a href="/about/" id="about">About</a></li>
</ul>


Notice each include file also contains a <cfimport> tag to reference the custom tag directory. Unfortunately, when you use <cfimport>, the custom tags are not available to included files.

When you browse the testtags.cfm page with the database and all custom tags in place, any content from the files above that is enclosed in tags with IDs will be magically inserted into the database. The next time you browse the page, the content will come out of the database to feed the page. Our database table looks something like this after browsing the sample page:

pageContentID pageContentLanguage pageContentPage pageContent pageContentTagId
1 1 _includes_menu_cfm Home home
2 1 _includes_menu_cfm Products products
3 1 _includes_menu_cfm Services services
4 1 _includes_menu_cfm Contact contact
5 1 _includes_menu_cfm About about
6 1 _includes_banner_cfm Eat at Joe's joes
7 1 _includes_banner_cfm Call 555-555-EATS phone
8 1 _testtags_cfm My Content goes here pagetitle
9 1 _testtags_cfm This tag has nothing interesting in it. Still, it has snazz because it's coming from the database. The following is total gibberish.  p2
10 1 _testtags_cfm Ten years ago a crack commando unit was sent to prison by a military court   for a crime they didn't commit. These men promptly escaped from a maximum   security stockade to the Los Angeles underground. Today, still wanted by the   government, they survive as soldiers of fortune. If you have a problem and no   one else can help, and if you can find them, maybe you can hire the A-team. p3
11 1 _testtags_cfm Mutley, you snickering, floppy eared hound. When courage is needed, you're   never around. Those medals you wear on your moth-eaten chest should be there for   bungling at which you are best. So, stop that pigeon, stop that pigeon, stop   that pigeon, stop that pigeon, stop that pigeon, stop that pigeon, stop that   pigeon. Howwww! Nab him, jab him, tab him, grab him, stop that pigeon now. p4
12 1 _testtags_cfm Ulysses, Ulysses - Soaring through all the galaxies. In search of Earth,   flying in to the night. Ulysses, Ulysses - Fighting evil and tyranny, with all   his power, and with all of his might. Ulysses - no-one else can do the things   you do. Ulysses - like a bolt of thunder from the blue. Ulysses - always   fighting all the evil forces bringing peace and justice to all. p5
13 1 _includes_footer_cfm Contents of this site copyright 2006 contents
14 1 _includes_footer_cfm Home home
15 1 _includes_footer_cfm About Us about
16 1 _includes_footer_cfm FAQ faq
17 1 _includes_footer_cfm Privacy Policy privacy
18 1 _includes_footer_cfm Contact Us contact

Conclusion

This part of the tutorial focused on getting the content management system in place with one language. In it, we showed the custom tag basetag.cfm which inserts existing content into the database as well as manages output of all content from the database. In the next part, we'll finish up with an administrative interface for changing content, and add the multi-language feature to change the language of the site with a click of a link.