It's generally better to assume that you'll want to reuse your code later for something else, so avoid building it around one specific use. Our translator module will consist of factory functions for different translation services (although we're only going to include one) that produce functions that perform actual translation work. Any translator function produced by a factory function should take the same arguments, but the various factories might have different arguments to represent different authentication criteria that the different services might use. We only need one factory for Microsoft Translator, which will need to know the client ID and shared secret in order to create a translator function that can authorize itself.
Create a new file in the TranslationBuddy
project directory, translation.lua
, and open it in the text editor of your choice.
First, we'll lay out the skeleton of the module as we've described it, and then we'll start fleshing out the factory for Microsoft Translator-enabled translation handlers.
local translation = {} function translation.microsoft(client_id, client_secret) return function(text, from, to, completion) end end return translation
Here we establish two things: what information is needed to create a translator that can draw on a specified Microsoft Translator subscription (the client ID and secret that we created and stored in credentials.lua
), and what information a translator engine needs in order to do a translation: the text to translate, the language that text is in now, the language you want it translated into, and a callback function. Because fulfilling a translation request may require network traffic (and will, in the case of our Microsoft module), it can't return the result as soon as the function is called. Instead, the translator receives a function as an argument that it will call with the finished translation once it's done. This is similar to the way you make a network.request
call; you provide it with a function that will process the result once you get it back, and then go on with whatever else you're doing.
We've already observed that your program might be able to take actions while it's waiting for the translation results, so we'll need to keep track of whether the translator engine is ready to receive requests.
function translation.microsoft(client_id, client_secret) local ready return function(text, from, to, completion) if ready then -- initiate translation return true else return false end end end
By returning true
or false
according to whether or not the engine was ready, programs using the engine to request translations can know whether they can expect their requests to be processed or not.
What we're creating is a pattern that computer scientists sometimes call a consumer, something that you keep supplying requests to and it keeps processing them. We'll create a function that loops infinitely, requesting new text to be translated, processing the translations, and passing them off to the callbacks supplied with them. Then, we'll integrate it with the pieces that will allow it to go into hibernation while it's waiting for a new translation request, or for a network response to arrive:
function translation.microsoft(client_id, client_secret) local ready, authorization local function translate(source) while true do local text, from, to, completion = source() local translation = fetch_translation(text, from, to, authorization) completion{ name = 'Translation', action = 'complete'; fromLanguage = from, toLanguage = to, source = text, result = translation } end end return function(text, from, to, completion) if ready then
Here's the basic form of our consumer function. It loops infinitely, collecting a translation request from a function provided to it when it starts. It calls another function to process that request using an authorization it maintains using the credentials it was created with (the arguments to the translation.microsoft
function). Finally, it constructs an event structure describing the results of the request and hands it off to the function that was supplied with the request. Then it lets the loop repeat.
Note the addition of the authorization variable. Microsoft Translator requires each request to be accompanied by an access token. The access token is requested from an authorization server, and is good for a certain amount of time (typically 10 minutes) from its issuance. The client ID and shared secret must be supplied when the access token is requested for the token request to be granted. So we need to make sure before any request that we have an authorization, and that it's up to date:
local text, from, to, completion = source() if not authentication or authentication.expiration <= os.time() then authentication = obtain_token(client_id, client_secret) end local translation = fetch_translation(text, from, to, authentication)
The way the function is designed right now, it tries to run continuously, which isn't going to work. It has to give control back to Corona whenever it shifts back into waiting mode, or the responses to its requests will never be delivered. The way to do that is by running it inside a Lua coroutine.
Coroutines are a little bit like threads in other multitasking environments (and in fact calling the type function on a coroutine object returns the string thread). They allow a function to be run semi-separately from the rest of the program, so that it can bookmark itself and go into a wait mode; when the main program calls it again, it picks up from the point where it bookmarked itself, as if nothing had happened.
To allow coroutines (and the main program) to communicate with each other, they can also pass values back and forth as arguments and returns when they hand off control. The first time a coroutine is started after being created, the body function can receive arguments through the start process and use them normally. The translate loop function takes one argument, a function that it can call to obtain the details of its next translation request.
Once it's ready to wait for the first request, the loop will yield control of the program back to the main thread that started it by calling a dedicated function, coroutine.yield
. Once the main program resumes the coroutine again, by passing it a translation request, the coroutine.yield
function will return the values that were passed into the restart process by calling coroutine.resume
. Let's start by seeing how this function works:
local ready, authentication local function submissions() ready = true local text, from, to, completion = coroutine.yield() ready = false return text, from, to, completion end local function translate(source)
This function releases control back to the main program and waits for the program to pass it the details of a translation request. So before it does that, it marks the lock
variable's ready
as true
so that the function that receives the requests will know not to refuse them. Once it's been restarted, it marks the translator as busy again so that if any duplicate requests are posted, they won't interfere with its progress.
The translator factory now needs to set up its coroutine and link the supply function to it:
source = text, result = translation } end end local self = coroutine.create(translate) coroutine.resume(self, submissions) return function (text, from, to, completion) if ready then
Notice that after creating the coroutine, we start it and pass it the submissions
function we created to collect requests. Since the first thing it does is call that function, the coroutine yields almost immediately, after setting ready
to true
. So the first time the control function (the function returned at the end of the factory) is called, it will resume the function from where it is paused, waiting to assign the particulars of the translation request.
This means we're ready to enable passing those requests into the coroutine when it's ready and the calling code has a translation request:
coroutine.resume(self, submissions)
return function (text, from, to, completion)
if ready then
coroutine.resume(self, text, from, to, completion)
return true
else
return false
end
end
end
Since we supply the various parameters as extra arguments to coroutine.resume
, and as the coroutine was already yielded, those parameters will be the return values from the coroutine.yield
call.
Every translation request requires an HTTP GET
call to a URL on a Microsoft Translator server. This call needs to be accompanied by an access token that was issued in the last 10 minutes; if no such access token exists, a new one needs to be obtained. We call functions for both of these operations in the code we've already created, but they don't exist yet. The trick is that each function has to yield and set itself up to be resumed in order to return.
local translation = {} local request_address = [[http://api.microsofttranslator.com/v2/Http.svc/Translate?text=%s&from=%s&to=%s]] local function fetch_translation(text, from, to, authentication) end function translation.microsoft(client_id, client_secret)
request_address
property is something of a constant. It describes the Internet address, as defined by the instructions for communicating with the Microsoft Translator engine, to which translation requests need to be sent. Notice the %s
tokens embedded in it; these can be replaced, by using Lua's string.format
function, with the values of the appropriate arguments.local function fetch_translation(text, from, to, authentication)
local self = coroutine.running()
end
local function fetch_translation(text, from, to, authentication) local self = coroutine.running() local function extract(event) end network.request(request_address:format(url.escape(text), from, to), "GET", extract, {headers = {Authorization = "Bearer " .. authentication.access_token}}) end
Bearer
followed by a space and the value of the access token.The extract
function, which we'll fill out in a moment, is supplied to network.request
so that it has a way of notifying our program once the request is complete.
network.request(request_address:format(url.escape(text), from, to), "GET", extract, {headers = {Authorization = "Bearer " .. authentication.access_token}}) return coroutine.yield() end
local function extract(event) local content = event.response:match("%b<>([^<>]+)%b<>") coroutine.resume(self, content) end
%b<>
search expression in Lua pattern matching searches for a less-than sign, a greater-than sign, and everything in between, allowing us to quickly and simply eliminate the tags and just capture the text content.By passing the string along to coroutine.resume
, we allow the function that yielded to pass it back as a normal return.
The process of requesting a fresh access token is actually very similar. The network address is different, the request is a POST rather than a GET, and the client credentials are stored in the request body rather than the URL, but the form, of posting a request, yielding, and then allowing the response to resume the thread is very similar:
local translation = {} local token_provider = [[https://datamarket.accesscontrol.windows.net/v2/OAuth2-13]] local token_request = [[grant_type=client_credentials&client_id=%s&client_secret=%s&scope=http://api.microsofttranslator.com]] local function obtain_token(client_id, client_secret) local self = coroutine.running() local function update(event) end network.request(token_provider, "POST", update, {body = token_request:format(url.escape(client_id), url.escape(client_secret))}) return coroutine.yield() end local request_address = [[http://api.microsofttranslator.com/v2/Http.svc/Translate?text=%s&from=%s&to=%s]]
The authorization information is actually returned as a JSON description of a record. Corona includes a function to build a Lua table equivalent to a JSON-encoded value, so converting it back will be trivial. The authentication response has four values, but we're only actually interested in two: the actual access token, and the duration it's good for (this is typically 10 minutes, but there's no reason to rely on that). Since it doesn't store a record of when the token was issued, we'll need to store that, too.
local function update(event)
local results = json.decode(event.response)
end
local function update(event)
local results = json.decode(event.response)
local now = os.date('*t')
end
local now = os.date('*t')
now.sec, now.isdst = now.sec + results.expires_in, nil
end
now.sec, now.isdst = now.sec + results.expires_in, nil
results.expiration = os.time(now)
end
results.expiration = os.time(now)
coroutine.resume(self, results)
end
The translation module should now be ready for action!
We constructed a blueprint for creating functions that accept translation requests, and implemented that blueprint for the specific service that was specified in our design. We used a coroutine to allow the function flow to act like the outside of a program rather than the inside, making the program flow more natural and easier to write. We also successfully engineered a two-way communication between our program and a remote server, submitting authentication credentials and storing a short-term authorization to complete requests.
If you want to test this part, try commenting out the contents of main.lua
and adding the code:
local translation = require "translation" local credentials = require "credentials" local translator = translation.microsoft(credentials.id, credentials.secret) local function report(event) print(event.result) end translator("Translation Buddy", "en", "ja", report)
The Corona Terminal should, after a moment, print out 翻訳の相棒. Testing code modules and individual functions is an important way to make sure you catch errors early, before you've written a lot of code that's dependent on something that may be broken.
Make sure you remove this code and uncomment the previous contents before proceeding.
3.133.138.177