• Blogs
Friday, May 07, 2004
06:52 pm UTC @Creator MightyE Delinquent Bills
Hits: 9269313
When I complained last month about Verizon's billing department having some screwy policies, I got a lot of very positive feedback. Little did I know when I wrote that, that many of you are also frustrated by companies who get so big that they can no longer bother to accurately account for their billing with us individuals.

Today I received a letter in the mail from Comcast. It's printed on a nice mint green paper, and says in bold letters at the top, "DISCONNECT WARNING." Dear customer, it says, our records show you have no idea how to pay a bill, and are late paying your current ones. Our billing department is infallible, and so you should send us lots of money for services for which you've already paid.

I'm no stranger to this particular letter. I've gotten it before, also from Comcast. The last 3 times though, it was printed on a pinkish-red paper rather than the nice mint green that it came in this time. Whereas before I felt like a bull in the streets of Madrid, this minty green paper makes me feel as though I'm skiing down the slopes of some famous skiing mountain, eating a peppermint patty. Well, so I assume, I've never really skied on any famous mountains with any sort of confection.

The last time I got one of these letters was about a month ago, which came at the tail end of a month and a half of effort working toward a resolution, and 3 similar letters before that one. At the time, Comcast stated I was delinquent on at least two bills some time between November and March; which months they couldn't say, but they were certain. Not just delinquent, they asserted that I never paid them at all, and certain enough that they were paying some other company to call me and harass me at work. I don't want to bore you with the gruesome details of this quest, but let me assure you, my coworkers had the radio off for the better part of a month as I talked to numerous different people within Comcast, each of which assured me they really had this thing solved this time. I knew I wasn't late on any bills, and that they'd all been paid, because I used my bank's automatic payment system, which sends the payment without my interaction on a schedule and amount that I specify. I had complete records showing I (or rather my bank) paid each bill on time, and these payments even happened entirely electronically so there was no check to lose.

I finally really honestly did get this all straightened out. A director of the billing department (who's apparently a level above a supervisor of the billing department, who's above a senior billing analyst, who's above a traditional billing analyst, who's above what ever company they pay to harass their customers) and I were on a first name basis, inquiring about each others kids and the like, and finishing phone conversations with, "Talk to you tomorrow." This latter part is rather an exaggeration, but not by much; I don't have kids.

So I was pretty surprised when I got yet another notice that my account is delinquent from a company which had all but offered a formal public apology for the inconvenience they'd caused.

I'll let you all know how I like DirecTV. Turns out you get a TiVO for cheap when you sign up.

MightyE
04:06 pm UTC @Creator MightyE So what's all this with the new translation system?
Hits: 9269344
I've already described in the previous blog how the old translation system works, and how it's not especially efficient. The difference between that system and the system I have partially deployed now is that the new system was based on a fundamentally different design concept. It's important to let people play the game in a non-English language, and I should alter my programming style to accommodate it. I don't have much time to describe it, but I'll do so briefly.

The new translation engine employs the sprtinf() function in PHP, which operates the same way that the one in C does. Those of you who have done some C development will recognize the sprintf() function, and can skip this brief description of it, picking up on the next paragraph. If I want to output that Farmboy Joe has 7 gems, and I'm using sprintf(), I do this: sprintf("~#%s~3 has ~^%d~3 gems.",$session['user']['name'],$session['user']['gems']). What this does is replace %s with the value found in $session['user']['name'] and %d with the value found in $session['user']['gems']. So if we're translating to German, we can change "~#%s~3 has ~^%d~3 gems." to "~#%s~3 hat ~^%d~3 Edelsteine." For more information on sprintf() under PHP, check out the PHP documentation.

If we need to change the order that the words appear in, we can do that also, by using this instead: "~^
%2$d~3 Edelsteine hat ~#%1$s~3." (I hilighted the parts that are significant). This is an important difference between the old translation system and the new one, where we can change the order of variables in the output under this system, while we couldn't before. Another important difference is that we can now be sure that we only replace entire strings of data, rather than having to look for partial replacements within the data.

Looking up an entire string can be accomplished much more quickly than having to look within a string. As this is something that happens very rapidly, we wanted to take advantage of a hard coded lookup procedure, and conveniently PHP's associative arrays provide us this opportunity, while also being very easy to implement. PHP uses strings as the index to their arrays and does lookups on them using an ordered map. Those familiar with the data structure called a hash table are already familiar with the map data structure, as hash tables are an implementation of maps. In the end, what this comes down to is that you can locate a given string in a giant pile of strings, with out having to compare against each string in the pile until you find a match. Instead you use cleverness to eliminate the majority of possibilities within the haystack, and are left with only a small handful of hay through which you have to look for your needle.

Now all we have to do is populate a PHP array with From => To pairs, and we can efficiently look up translation pairs. It is still a little hairy to have to dig through code and locate all the output strings though, so each time text goes through the translator now, those who have the translator tool turned on will see a tiny T appear. The T will be red for those spots that no translation pair is found in their current language, and blue for any that were found. Clicking the T brings up a window that shows the the text that went through the translator at that point and allows them to specify a translation, establishing the pair.

The system will also collect previously unseen translation calls, and allow the translating player to pour over a list of untranslated text, providing translations for them.

As you may have guessed, translation pairs are being stored in the database, that's the only feasible and universally functional way to store data that's being updated dynamically like this. It's not a good idea however to load the text for the entire game each page hit, when only portions of the text are going to be needed on any given hit. So we introduced the idea of namespaces. When the engine enters a new translation namespace, it loads the pairs for that namespace if it hasn't already, and translation pairs that weren't defined in that namespace aren't examined. This lets us put, for example, all the buff output messages in the "buffs" namespace, and then no matter what page they're called from, the text will display properly, while not having to load text from the inn while we're in the forest. The namespaces operate as a stack, so if while in a forest fight, we do something with buffs, we would push "buffs" on the stack, and when we're done with the buffs, we pop "buffs" off the stack, and the translation namespace returns to whatever it was before we started working with buffs.

We automatically set a number of namespaces. For example, all page hits default to the script name as the name space. Any time we're executing code within a module, we're in a namespace tied to that module's name. We don't have to worry about that. The only times we need to worry about setting a namespace is when certain text appears in more than one place, such as with the buffs, or the Vital Info in your nav over there. Buffs, as I mentioned, go in the "Buffs" namespace, and vital info goes in the "global" namespace (reserved for those texts which appear on every page, eg, "MoTD," "Ye Olde Mail: %d new, %d old," and "Petition for Help".

If a multi lingual admin installs a module, changes a buff, or does something else which leaves some text untranslated, they have only to click the T and provide a translation, and they're fixed. No need to dig in code at all, and we could grant rights to anyone we want to provide a translation in any language they know.

In the future, we may be taking applications for translators, but we're not just now; no need to barrage me with requests to be a translator just yet =). The reason for this is that the system isn't totally done and it's not yet ready for public consumption.

I hope this satiated the sick curiosity a number of you have demonstrated in to how we are running the translator =). I'm fairly flattered that so many are so curious as to how I structure things, though I can't take credit on all that structural stuff, JT spends many an hour telling me why I'm an idiot for structuring things a certain way (I've spent a few reciprocating that, but not as many).

Thanks for those who were interested, and for those who weren't, why are you still reading by this point in the blog? You're crazy!

MightyE
07:39 am UTC @Creator MightyE Boring stuff about the translation engine
Hits: 9269389
Seems not a few of you are curious about this translation engine thing I mentioned before. So here's the gory details.

To appreciate the new translation system, a bit of understanding about the old translation system is in order. A design principle I took when approaching the old translation system was, "I speak the language this game was written in, so I don't really get much out of a translation engine, thus it shouldn't really affect the way I approach my main code." Toward that end, I built it in such a way that it was compatible with all the back code I'd done at the time.

In order to specify a translation on a given line of code, the person translating would open translator_XX.php (where XX is the language code) and find the section in that file that indicates the page they're working on. If they were going to translate the sentence "Mary has a little lamb" they'd add this to that section:
"Mary has a little lamb"=>"Mary hat ein kleines Lamm"
and whenever "Mary has a little lamb" on the page in question was output, this would be replaced with the translated version. As soon as the engine located a valid translation pair, it stopped looking for others for that bit of output for efficiency reasons. That works ok, except where I include variables inline with the rest of the output.

If I was going to output how many gems the current player has, I'd do this:
output("~#{$session['user']['name']}~3 has ~^{$session['user']['gems']}~3 gems.");
where I'm using the ~ instead of the backtick (or grave to you frenchies) since there's no way to represent one in a blog with out it thinking it's a color code =). The output of that would look like this:
Farmboy Joe has 7 gems.
Now this represents a problem, because the variables are put in place by the PHP engine, and by the time it reaches the translator, "Farmboy Joe" and the "7" are already there. You obviously don't want to do translation pairs for every combination of player names and how many gems they might have. So I made it so that any one line could have multiple replacements done on the same line. To do this, the translating party simply specified an array of possibilities:
array("has"=>"hat","gems"=>"Edelsteine"),
as one line in the translation script. If the engine got to this line and saw a "has" it would do all the replacements in the array, and stop looking for other translations. This successfully translates the gems line, but you have to be careful about the ordering of this. Because "has" is a common word, you need to put it low down in the listing so that another line doesn't get translated by this pair first.

It was functional if a little difficult to tweak, reorganizing lines here and there if a translation happened out of order. But it was hugely inefficient. Each output that went through the translation engine required one substring replacement for each translation pair. Well, not exactly, since it stops for the first successful pair that it finds. So the growth order of this algorithm is somewhere about O = n log(n). The log(n) is the searching for translation pairs, and the n is the length of the string we're searching through. That's not a superb growth order, we'd prefer to see something like O = n, or O = log(n), but at least it's not O = n^2. What all that fancy talk means is simply that the more data we put in this thing, the less efficient it gets. Once you get an entire game pushed in to the translator, it comes to a crawl. This is something I didn't see when developing it in the first place because I only translated bits and pieces just as a technical (not scalar) test of the system. Honestly it wasn't important enough to me to pursue it in more depth than that.

Well, the new translation system should be much closer to a log(n) growth, which means that if you're doing almost no translation, you see a higher price paid for this system over the old one, but as you begin to grow the translation or the output that you're doing, it surpasses most other translation systems by far. Unfortunately I ran out of time just now to talk about how that works, I've got to go to work. I'll try to toss an entry on that up here later today =).

MightyE
Creative Commons License This work is licensed under a Creative Commons License.
Game Design and Code: Copyright © 2002-2008, Eric Stevens & JT Traub
Design: Jade Template © Josh Canning 2004 of HFS
View PHP Source
Version: 1.0.6+classic
(Page gen: 0.06s, Ave: 0.06s - 3.54/60)