Programmi täitmine (execution) toimub alati mingis konkreetses operatsioonisüsteemis ja konkreetses protsessoris. Kuna protsessorid ja operatsioonisüsteemid on erinevad, vajatakse klassikalise transleerimissüsteemi järgi transleeritavate keelte (C, C++ jne) iga keskkonna (protsessor+operatsioonisüsteem) jaoks oma translaatorit, mis teisendab talle esitatud programmi teksti just selle protsessor+operatsioonisüsteemi jaoks: Windows-PC jaoks oma (ja erinevate Windows-i versioonide - 16-bitise, 32-bitise ja 64-bitise jaoks samuti erinevat), Linux-PC jaoks oma, Unix-tööjaamade jaoks oma jne. On arusaadav, et sellise tehnoloogia kasutamisel vajatakse programmeerimiskeele jaoks palju translaatoreid ja on väga raske tagada, et programm esinevates keskkondades annaks alati täpselt sama tulemuse. Olukorda muudab veel keerulisemaks see, et translaatoreid valmistavad erinevad firmad (Microsoft, Borland, IBM jt), kes sageli tõlgendavad programmeerimiskeele kirjeldust/standardit veidi erinevalt; tulemuseks on olukord, kus peaaegu alati mingi suurema programmi täitmise tulemustest rääkides mainitakse kindlasti ka see, millise translaatoriga programm transleeriti ja millises keskkonnas see käivitati, sest erinevate translaatorite ja keskkondade puhul on tulemused sageli (veidi) erinevad.
Erinevat lähenemist kasutati Java keele loomisel - programm transleeritakse kahes etapis, esimeses etapis on tulemuseks konkreetsest keskkonnast sõltumatu, s.t. kõigi arvutite/opsüsteemide jaoks ühesugune vahekuju (Java baitkood, bytecode), mis alles teises etapis tõlgendatakse konkreetse protsessori/opsüsteemi jaoks sobivale kujule. Kuna vahekuju on väga madala tasemega, on teine etapp suhteliselt lihtne ja seda sooritav programm VM (Virtual Machine, virtuaalarvuti) suhteliselt väike, see võimaldabki Javat kasutada väga piiratud võimalustega keskkondades, näiteks mobiiltelefonides. Java puhul oli see "leiutis" tegelikult poolsunnitud, sest Java-t ei planeeritud esialgselt üldse üldkasutatavaks programmeerimiskeeleks, vaid interaktiivse televisiooni jaoks (Javat hakati arendama 1991, kui Intenet-i praegusel kujul polnud üldse veel olemaski) ja Java oli planeeritud televiisorite lisaseadmete, nn "set-top" seadmete programmeerimiseks; kuna need oleks olnud samuti piiratud võimalustega ja väga erinevad, pidi transleerimise kavandama nii, et see viimane etapp oleks võimalikult lihtne. Kuid mitmesuguseid vahekeeli oli kasutatud ka varem, paljudes programmeerimiskeeltes ja -süsteemides kasutatav täitmissüsteem (run-time system) on samuti sisuliselt virtuaalmasin, kuid kuid tavaliselt vaid mingi konkreetse protsessori+opsüsteemi jaoks.
Sama vahekeelt kasutatakse sageli mitme erineva programmeerimiskeele translaatori jaoks, näiteks GNU gcc translaatorite süsteemis kasutatakse vaheleelt RTL (Register Transfer Language) nii C, Ada, Fortrani kui ka teiste gcc translaatorite süsteemis realiseeritud keelte transleerimisel; sama vahevormi kasutamine lihtsustab ka nn rist-translaatorite (cross-translators) loomist, mis transleerivad ühest kõrgkeelest (näiteks Pascal-ist) teise (näiteks Ada-sse).
Java eeskujul võttis ka Microsoft oma .net programmeerimissüsteemis kasutusele vahekeele MSIL (Microsoft Intermediate Language), esimesena kasutatati seda programmeerimiskeele C# transleerimisel. MSIL-i virtuaalmasinat/täitmissüsteemi nimetatakse .net-ümbruses CLR (Common Language Runtime) ja see on projekteeritud nii, et seda saaks kasutada paljude programmeerimiskeelte transleerimisel: APL, C, C++, C#, COBOL, Eiffel, Forth, Fortran, Hasell jne - selle omadused (sisseehitatud andmetüübid ja käsud) peaks sobima rohkem kui kahekümnele programmeerimiskeelele.
Translaatorite teoorias on kõige tuntumaid vahekeeli nn kolmeaadressiline kood. Kolmeaadressilise koodi versioone on mitmesuguseid, erinevate operatsioonide ja andmestruktuuridega, kuid neis kõigis kasutatakse vaid järgmise kujuga arvutuskäske:
x = y op z
x = op y
x = y
"Kõrgema taseme" kolmeaadressilises koodis võivad olla lubatud ka alamprotseduurid; nende argumentide tähistamiseks kasutatakse võtmesõnu param, call ja koodilõik
param x1
param x2
...
param xn
call p, n
Kolmeaadressilise koodi töö juhtimiseks kasutatakse vaid suunamiskäske kujul
goto M
if x relop y goto M
Sellised suunamiskäsud on realiseeritud ka enamuses protsessoritest, sellepärast on kolmeaadressilise koodi teisendamine masinkoodiks või assembleriks väga lihtne, kui näiteks x,y on 32-bitised täisarvud, vastab kolmeaadressilise koodi käsule
x = y + z
järgnev kõrgtaseme assembleri HLA koodilõik
mov(y, eax)
mov(z, ebx)
add(eax, ebx)
mov(ebx, x)
Vahekeelena võib kasutada ka Java baitkoodi tekstilisi kujusid; Java virtuaalarvuti VM jaoks on juba loodud üle kahesaja mitmesuguse keele.