To help the user with common phrases and save bandwidth and translation allowance, the app will store a history of requested translations and their results. We'll give the user the option to view them, and use them to avoid duplicate network requests if the user enters text that has already been translated.
We'll store the history file in the app's Documents
folder, which sync software generally backs up to the user's computer. Each line will be a JSON-encoded copy of the arrays of strings that holds the original text and the translation.
Open the main.lua
file in the TranslationBuddy
folder, if you don't already have it open from the previous task.
The history view and the entry view both need access to the history file, so it's important for them to agree on its location. Lua doesn't have symbolic constants, but we can create a global variable and never change it; both modules can then share it as a common file path.
main.lua
, before the storyboard calls, add a line to define that location, in the directory that gets backed up by sync software:display.setStatusBar(display.DefaultStatusBar)
PATH = system.pathForFile("translation.history", system.DocumentsDirectory)
local storyboard = require "storyboard"
PATH = system.pathForFile("translation.history", system.DocumentsDirectory) io.open(PATH, 'a') :close() local storyboard = require "storyboard"
main.lua
for the moment and modify entry.lua
to use this file as a cache. Find the scene:userInput
function; if we already have the translation saved, there's no need to send it to the translation engine. We'll look at each line, and see if its array starts with the entered text:local originalText = event.target.text for line in io.lines(PATH) do local history = json.decode(line) if history[1] == originalText then displayResults(history) return end end if translator(originalText, 'en', 'ja', output) then
If we find such an array, which indicates that the text was translated before, we'll simply proceed to the results screen using that saved text, and bail out of the function without calling the translator.
output
function that handles translation results: storyboard.gotoScene( "result", {effect = 'slideLeft'; params = texts })
end
local function output(event)
native.setActivityIndicator(false)
local texts = {event.source, event.result}
save(texts, PATH)
displayResults(texts)
end
local translator = translation.microsoft(credentials.id, credentials.secret) local function save(entry, path) local log = io.open(path, 'a') log:write(json.encode(entry), ' ') log:close() end local function displayResults(texts)
At this point, the entry module is silently caching translation results in order to save service costs. However, the design also calls for the app to let the user view this history and review previous translations. This calls for another scene. Save entry.lua
, and copy scenetemplate.lua
into a new scene file, history.lua
. Open this file in your preferred editor.
The history view will use a table view to display the English lines that were sent to the translator. Like the result module, it will create the table fresh each time the scene is launched, but it's even simpler; it needs no extra controls, so no stratum is required:
function scene:enterScene( event ) local group = self.view self.History = widget.newTableView{ id = "translation_history"; width = display.contentWidth, height = display.contentHeight; topPadding = display.statusBarHeight, bottomPadding = 50; } group:insert(self.History) end -- Called when scene is about to move offscreen: function scene:didExitScene( event ) self.History:removeSelf() end
Similarly to the result module, the history will use an array table to store all the translation requests to display or execute. However, it will use single-line text objects to display just the first part of the English submissions, and each entry will be based on one line in the history file:
local group = self.view self.Data = {} local function displayHistory(event) display.newText(event.view, self.Data[event.row.index][1], 4, event.row.height * 0.125, native.systemFont, event.row.height * 0.75) :setTextColor(0x00) end self.History = widget.newTableView{ id = "translation_history"; width = display.contentWidth, height = display.contentHeight; topPadding = display.statusBarHeight, bottomPadding = 50; onRowRender = displayHistory; }
The table will fill in this array with lines from a text file:
group:insert(self.History) for line in io.lines(PATH) do table.insert(self.Data, (json.decode(line))) self.History:insertRow{ height = display.contentHeight * 1/12; } end end
Unlike the result view, rows in this table should be selectable to load their contents in the result view. Table views make this easy by taking an onRowTouch
handler that processes touch events on the rows and notifies them when they've been pressed, released, or swiped. We're only interested in releases in this case.
The function is basically a copy of the displayResults
utility function in entry.lua
:
:setTextColor(0x00) end local function loadHistory(event) if event.phase == 'release' then storyboard.gotoScene("result", {effect = 'slideLeft'; params = self.Data[event.row.index]}) end end self.History = widget.newTableView{ id = "translation_history"; width = display.contentWidth, height = display.contentHeight; topPadding = display.statusBarHeight, bottomPadding = 50; onRowRender = displayHistory; onRowTouch = loadHistory; }
We want users to be able to request translations through either the new entry or the history view; there's an established convention for this in mobile apps, and Corona provides support for it through the tab bar widget. A tab bar usually takes up the bottom of the screen and has a few icons, often with text labels, that can be tapped to switch between different pages of an app's interface. Like the other widgets, the tab bar is highly customizable, but its default appearance is frequently quite adequate, so the only things you have to specify are where to put it and what buttons to put on it.
main.lua
and load the widget
module:io.open(PATH, 'a')
:close()
local widget = require "widget"
local storyboard = require "storyboard"
storyboard.gotoScene( "entry" ) widget.newTabBar{ top = display.contentHeight - 50; buttons = { { id = "entry"; label = "Translate"; width = 32, height = 32; defaultFile = "presentation/translate-up.png", overFile = "presentation/translate-down.png"; onPress = pickScene; selected = true; }, } }
The id
field for a button can be any Lua value you care to use, and I use the names of the scenes they will load to keep the code simple. The label
field is a string that will be displayed under the icon for the tab. It can be blank, but leaving icons unlabeled makes them much less useful; very few are as immediately intuitive as their creators believe. The width
and height
fields are mandatory in current versions of the widget API, and just specify the dimensions of the button icon. The defaultFile
and overFile
fields specify the icons to be used for the tab when it is selected (over) or deselected (default). The selected value should only be set on one button, and indicates that that button should use its up image when the bar is loaded. The onPress
field should be a function that handles presses on the tab; it can be shared between different buttons if they have some way for the function to distinguish them (like the id
value).
buttons = { { id = "entry"; label = "Translate"; up = "presentation/translate-up.png", down = "presentation/translate-down.png"; onPress = pickScene; selected = true; }, { id = "history"; label = "History"; width = 32, height = 32; defaultFile = "presentation/history-down.png", overFile = "presentation/history-up.png"; onPress = pickScene; }, }
storyboard.gotoScene( "entry" ) local function pickScene(event) storyboard.gotoScene(event.target._id) end widget.newTabBar{
Now you can load the app and switch between the two views. However, unless your history only consists of very small phrases, you may notice an odd glitch when you select a sentence from the history.
To keep the table short, the history view displays only the beginning of each English sentence in a single-line text object. However, the rest of each text is still hanging off the right edge of the screen. This isn't an issue until the scene slides left to make room for the result display, dragging the extra text across the view. We'll fix this with a mask.
The file presentation/masking-frame.png
is pretty simple. Since the screen size is specified in the config.lua
file as being 320 x 480, the mask file consists of a white area (which will reveal everything in its target object normally) surrounded by a 4 pixel black border on all sides. Black prevents anything in the target object from showing up at that spot. So, using this mask crops out everything around the edges of the target screen. We'll load the mask with the history module, and attach the mask to the view whenever it's created:
local json = require "json" local frame = graphics.newMask("presentation/masking-frame.png") -- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view group:setMask(frame) end
The mask can be positioned on its target using the maskX
and maskY
values, which specify where to put the center of the mask compared to the masked object's local origin. This is usually the center of the object, but for groups, it's whatever point is defined as the group's point (0, 0). That's frequently the top-left corner, depending on where you placed the group's children inside it. So, we'll move the mask to line up with the center of the screen:
function scene:createScene( event )
local group = self.view
group:setMask(frame)
group.maskX, group.maskY = display.contentCenterX, display.contentCenterY
end
If you try it again, you should not see any more spill-over from the history view into the results as they slide in!
We built a scene that uses a slightly more complex table view. We populated it with data from a file rather than a table, and made it respond to touch actions to select the specified entries. We also created another commonly-used, familiar user interface object to switch between the two related tasks the app can perform; viewing old translations and viewing new translations.
When creating tab bar icons, it's common to leave any part that might be considered the background of the icon, transparent so that it can be superimposed over any bar or background. It's also typical to make some distinctive difference between the selected and unselected forms of an icon, such as grayscale versus color, or flat versus embossed.
To work properly with the graphics rendering engine, images used as masks must have a height and width that are multiples of four, and they should have a border all the way around the edges of black pixels, at least 3 pixels thick. Grays can also be used to make the masked target partially transparent. We'll explore masks in detail in Project 6, Predation
13.58.39.23