Assembling the translator

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.

Getting ready

Create a new file in the TranslationBuddy project directory, translation.lua, and open it in the text editor of your choice.

Getting on with it

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.

Gatekeeping requests

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.

Consuming requests

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.

Maintaining authorization

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)

Linking the translation loop

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.

Handling the network requests

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.

  1. We'll start with the one that gets called on every translation request:
    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)
  2. The 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
  3. This records the currently running coroutine. We'll need this information to make sure that we can resume the right one once the response to our request is received.
    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
  4. If you're not familiar with how web server communications work, each request or response placed via the Hypertext Transfer Protocol contains a body and a number of headers, although either can be empty. Each header has a name and a value. The Microsoft Translator API specifies that each request needs a header called Authorization whose value is the word 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.

    Tip

    The function url.escape converts arbitrary text into a form suitable for inclusion in URLs by replacing illegal characters with escaped equivalents. It's part of the LuaSocket library, created to add low-level networking capability to Lua, and included in Corona.

      network.request(request_address:format(url.escape(text), from, to), "GET", extract, {headers = {Authorization = "Bearer " .. authentication.access_token}})
      return coroutine.yield()
    end
  5. Having filed our network request, we sit back and wait for the results to be returned to us. But in order for those results to make it to the right place, we have to make sure that the response function sends them there.
      local function extract(event)
        local content = event.response:match("%b<>([^<>]+)%b<>")
        coroutine.resume(self, content)
      end
  6. The response comes back as a very simple XML document, one that just has a single tag. While we could write or use a library to parse XML into Lua data structures, such an operation is serious overkill for a result that will consistently be only a single tag and its text contents. The %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.

Renewing the access token

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.

  1. We convert the encoded response into a table with separate fields:
      local function update(event)
        local results = json.decode(event.response)
      end
  2. In order to preserve the time the token was issued, and more importantly, when it expires, we collect the current time, broken down into a table with separate fields for the different components: hours, minutes, and for our purposes, seconds, which is what the token's duration is provided in. So now we're going to adjust that time:
      local function update(event)
        local results = json.decode(event.response)
        local now = os.date('*t')
      end
  3. We make two modifications to the time we've gotten. We add the duration of the token in seconds to get the expiration time. We also clear any specification about whether the time provided is in Daylight Savings Time or not. This is only rarely a problem; it only makes a difference if the current time and the specified time are on opposite sides of Daylight Savings Time starting or ending. But when it does occur, it leads to bugs that are extremely frustrating and hard to pinpoint, such as causing you to label a token that lasts 10 minutes as not expiring for an hour and 10 minutes:
        local now = os.date('*t')
        now.sec, now.isdst = now.sec + results.expires_in, nil
      end

    Tip

    Make a habit of clearing DST data from time specifications whenever you use a time and date table to increase, or decrease, a duration or point in time by a certain amount. I spent multiple days isolating a bug in a Lua program that was caused by automatic DST adjustment.

  4. Record the expiration time into the authorization object so that we can track how much time it has left:
        now.sec, now.isdst = now.sec + results.expires_in, nil
        results.expiration = os.time(now)
      end

    Note

    Feeding the table back into os.time converts it into a pure numerical measure of time that can be compared as being less (earlier) or more (later) than another time.

  5. Finally, return the usable authorization object back into the thread that's waiting on it:
        results.expiration = os.time(now)
        coroutine.resume(self, results)
      end

The translation module should now be ready for action!

What did we do?

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.

What else do I need to know?

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.138.177