For those who don't know, I am Canadian. Canada has two official languages; English and French and to promote our culture, we have language laws. We also have a remarkable ability to turn a three-word button label into a meeting with six stakeholders and a lawyer. So when I say an application needs translation, I don’t mean changing “Welcome” to “Bienvenue” and leaving the rest in English. I’ve tried that approach. It turns out people can read past the first word.
ColdFusion can now call an AI model without us assembling web requests by hand. We give ChatModel() a provider, a model name, and credentials, then call .chat() with a prompt. ColdFusion returns a response containing the model’s message. The model itself is stateless: it doesn’t remember the previous call or maintain a conversation for us. That suits translation rather well. I don’t need the model developing a relationship with our page titles. Check out Adobe’s ChatModel documentation for more about this.
Over this six-part series, I’ll build a pipeline that finds content needing translation, sends it in manageable pieces, checks what comes back, and saves the result for human review. Generating text and publishing text will remain separate operations. That separation is important when the machine confidently translates a contact page into what appears to be a ransom note.
Today’s goal is smaller: translate one plain-text field through ColdFusion’s native AI interface. I’ll put the provider call behind a component, keep credentials out of the code, and test the translation service without paying for a model request.
Get ColdFusion ready
This example requires Adobe ColdFusion 2025 Update 8 or later and the ai package. The package may need to be installed separately after updating ColdFusion. You can install it through the ColdFusion Administrator’s Package Manager or with the ColdFusion Package Manager:
cfpm install aiRun that against the ColdFusion installation serving your application, not whichever installation happens to be easiest to find. Computers are remarkably willing to let us fix the wrong one. Check out the Adobe’s AI setup guide, and ColdFusion Package Manager documentation.
We’ll use OpenAI as the provider in the example, but we’ll keep that choice inside one component. Set these environment variables for the ColdFusion process:
TRANSLATION_AI_ENABLED=true
TRANSLATION_AI_API_KEY=your-provider-key
TRANSLATION_AI_MODEL=your-supported-model-nameThe model name must be one your provider account can use. Don’t put the key in a ColdFusion template, a JavaScript file, or a helpful screenshot of your server settings.
My example has three files:
components/
NativeTranslationModel.cfc
TranslationService.cfc
translation_smoke.cfmPut the model call behind a component
Create components/NativeTranslationModel.cfc:
component {
public string function generate(required string prompt) {
var system = createObject( "java", "java.lang.System" );
var enabled = system.getenv( "TRANSLATION_AI_ENABLED" );
if ( isNull( enabled ) || compareNoCase( trim( toString( enabled ) ), "true" ) != 0 ) {
throw(
type="TranslationAI.Disabled",
message="Translation is disabled."
);
}
var apiKey = system.getenv( "TRANSLATION_AI_API_KEY" );
var modelName = system.getenv( "TRANSLATION_AI_MODEL" );
if (
isNull( apiKey ) || !len( trim( toString( apiKey ) ) ) ||
isNull( modelName ) || !len( trim( toString( modelName ) ) )
) {
throw(
type="TranslationAI.NotConfigured",
message="Translation is not configured."
);
}
var response = {};
try {
var model = ChatModel( {
provider: "openAi",
apiKey: toString(apiKey),
modelName: toString(modelName),
timeout: 20,
logRequests: false,
logResponses: false
} );
response = model.chat( arguments.prompt );
} catch ( any error ) {
throw(
type="TranslationAI.Unavailable",
message="The translation model is unavailable."
);
}
if (
!isStruct( response ) ||
!structKeyExists( response, "message" ) ||
!isSimpleValue( response.message ) ||
!len(trim(toString( response.message ) ) )
) {
throw(
type="TranslationAI.EmptyResponse",
message="The translation model returned no text."
);
}
return trim( toString( response.message ) );
}
}A few choices here are deliberate.
The component reads the key when it makes the call. It doesn’t store the key in application scope or return it to its caller. The feature switch defaults to off: anything other than the literal value true disables translation.
logRequests and logResponses are both set to false. Adobe documents these as model configuration options; we don’t want a well-meaning diagnostic setting writing page content or generated translations into provider logs. Make sure you read Adobe’s model configuration reference.
Finally, construction and request failures become a generic application error. I can add safe, useful diagnostics later. Passing a raw provider exception to the browser is an efficient way to tell strangers more about my infrastructure than they asked to know.
Give the application a translation service
The model component knows how to generate text. It shouldn’t decide which languages I'm translating between or build every feature’s prompt. Create components/TranslationService.cfc:
component {
public TranslationService function init( required any model ) {
variables.model = arguments.model;
return this;
}
public string function translate(
required string sourceText,
required string sourceLocale,
required string targetLocale
) {
var text = trim( arguments.sourceText );
if ( !len( text ) || len( text ) > 1000 ) {
throw(
type="Translation.InvalidSource",
message="Source text must contain between 1 and 1000 characters."
);
}
if (
!reFind( "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$", arguments.sourceLocale ) ||
!reFind( "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$", arguments.targetLocale ) ||
compareNoCase( arguments.sourceLocale, arguments.targetLocale ) == 0
) {
throw(
type="Translation.InvalidLocale",
message="Choose different, valid source and target locales."
);
}
var prompt =
"Translate the source_text value from " &
arguments.sourceLocale & " to " &
arguments.targetLocale & ". " &
"Return only the translation, with no explanation." &
chr(10) &
serializeJSON({source_text: text});
var translated = variables.model.generate( prompt );
if (!isSimpleValue( translated ) || !len( trim( toString( translated ) ) ) ) {
throw(
type="Translation.InvalidResult",
message="The translation was empty."
);
}
return trim( toString( translated ) );
}
}I'm limiting this first version to 1,000 characters. That’s not my final pipeline limit; it’s a guard against accidentally feeding an entire article into a demonstration built for one field. Long content gets its own treatment later in the series.
The source text is serialized as a JSON value so it’s visibly separate from the instruction surrounding it. That doesn’t make hostile source content harmless. I'll deal with input screening and stronger output validation in part three. For now, use this service with content you control.
Test without calling the provider
The translation service accepts a model object when I create it. That lets me replace the paid model with a tiny fake one:
<cfscript>
fakeModel = {
generate: function( required string prompt ) {
if (
!find( "Hello", arguments.prompt ) ||
!find( "fr-CA", arguments.prompt )
) {
throw( message="The prompt is missing expected content." );
}
return "Bonjour";
}
};
service = new components.TranslationService( fakeModel );
result = service.translate(
sourceText="Hello",
sourceLocale="en-CA",
targetLocale="fr-CA"
);
if (result != "Bonjour") {
throw( message="Translation test failed." );
}
writeOutput(" Translation service test passed." );
</cfscript>That test proves my validation, prompt construction, and model boundary work without an account, a network request, or an invoice. The fake model can’t tell me whether the real provider will produce a good translation. It can tell me whether my own code is wired together correctly, which is a useful indignity to establish before spending money.
Make one live call
Once the package and environment variables are configured, create translation_smoke.cfm:
<cfscript>
service = new components.TranslationService( new components.NativeTranslationModel() );
translated = service.translate(
sourceText="Welcome to our website",
sourceLocale="en-CA",
targetLocale="fr-CA"
);
writeOutput( encodeForHTML( translated ) );
</cfscript>Run this in a local or protected environment, then remove or protect the page. An unauthenticated demonstration endpoint connected to a paid model is less a tutorial and more a charitable contribution to whoever finds it first.
I now have a ColdFusion translation call, a clean boundary around the native model, and a test that doesn’t need the provider. I don't have a safe publishing pipeline. The translation hasn’t been checked for altered names, missing placeholders, invented facts, or creative interpretations of the source. It hasn’t been saved or approved.
In part two, I’ll build the inventory that tells this service what needs translating. Otherwise, my sophisticated pipeline is just a function that says “Bonjour” when we poke it.