Click here to Skip to main content
15,867,835 members
Articles / Programming Languages / XML

Latin Textbook and Look-up Table in C#

Rate me:
Please Sign up or sign in to vote.
4.83/5 (27 votes)
23 Apr 2010CPOL63 min read 55.5K   1.7K   28   26
Wheelock's Latin textbook with Cassell's Latin Dictionary, written in C#.

Why Latin?

(This Article previously published is updated with 2019 code revamp with new features)

For years now, I've been making an effort to learn a bit of Latin. Though the learning has been considerably slow, it's given me the opportunity to write a Latin project which has been helpful along the way. In a previous article, Spell Weller, but Grammar's Up to You, I make reference to this Latin project without providing any code, and had some haunting qualms about this remiss. So I determined to dedicate an entire article exclusively to this project and reformat the data in order to make it possible for you to upload it onto this venue and make it publicly available.

The reason why I decided to try to Latin in the first place may have been due to a small-c catholic upbringing which could do nothing with my innate atheistic ways, or it may have stemmed from the night I cracked a rib watching Monty Python's Life of Brian.

Latin_textbook/romani_ite_domum.jpg

Whatever the case may be, you'll find that this application is better than any other Latin project you'll find anywhere. One program that compares is Whittaker's Words, which does much of what this app's Look-up Table does in a DOS window interface. Not as user-friendly as it might otherwise be, but given its download file size and ease to install, it is really quite remarkable! This latest (2019) version includes some new features like : 

  • Tool tips - helpful tips that guide you throughout the user-interface
  • Dictionary content search - essentially an English-to-Latin dictionary
  • Spell-checker
  • Library of Ancient Latin manuscripts from Roman times to Middle Ages and Renaissance
  • Library content search - to help you find the texts you're looking for
  • Easy to use Dictionary Editor Interface - to create new or edit existing word entries
  • Administrator Login - to prevent less-than-responsible users from tampering with your Latin!!!

To start with, I had to personally type the entire first-half (Latin to English from A-Z) of Cassell's Latin/English-English/Latin Dictionary (24000 word entries!) when I first began my Latin studies using Wheelock's Latin textbook about three years ago. Over a two year period, I then wrote the first version of this program in VB in a piece-meal fashion by adding whatever new conjugations or declensions I learned from my hard-cover Latin textbook into the project so that my computer could ease the learning process.

Then, about a year ago(2009), I rewrote the whole thing in about one month using C#. This (first publishing)newer version was much, much better. Since the original VB was so ill-planned (not planned at all!), that C# re-write was considerably faster and more efficient.  I then, I've also typed in the Wheelock's Latin textbook, and made some modifications to the files which were originally written in a text file format and are now in XML, with formatting information taken from RTF files by way of code from the Writing Your Own RTF Converter CodeProject article which greatly facilitated the task.  I was locked away in a penitentiary for seven years, woeth me, and came out of there eager to get back to work on this and other projects.  Where the older version's search engines needed to be rebuild by the user after downloading this version's search engine data-files are broken down to fit the Code-Project's allowable file upload size of 10MB.  The transition to the newer system resulted in some acceleration of file retrieval which I had not foreseen, which is always a bonus.

The most difficult part in writing the original VB version was teaching the code to figure out what type of word each word entry in the Cassell's dictionary was. Is it a noun? An adjective? Which declension? Verb? Adverb, indeclinable, pronoun, and so on, because that dictionary did not have all the information that a Latin scholar would know in simply looking at an entry. Definitions that started with the word 'to', like 'to run' or 'to write', were assumed to be verbs. Most nouns have only two principal parts, but so do some adjectives. In short, the whole process of getting this working properly took the better part of a frustrating two years, and I had no intention of going through all that again in the C# rewrite, so I used the VB code to copy every word entry from the first hand-typed version, figure out what each word was, then store the word-type information in the word's definition and save it in a separate directory. That done, I was then prepared to take on the C# rewrite, which you have here.

More recently, I've gone over the same dictionary files with a bit of code to find all the 'indeclinables' that I had neglected to fix and label properly by hand.  There were thousands.  Luckily I was able to redo many of these automatically with more precise lines of code to deal with certain cases, many of them needed to have their headings re-written and after about a week of wrangling with those the lot of them seem much better.  There is now a Dictionary Entry Editor interface which is far better than the previous cryptically-written less-than-friendly and sometimes-buggy version that was prone to over-write valid entries to newer not-so-valid word entries.  ... but I'll explain that a little later.

You have to understand that I publish this article, the software, and the copyright protected Cassell's Dictionary & Wheelock's Latin textbook at the risk of receiving angry e-mails from this guy:

Latin_textbook/this_guy.jpg

who recorded the audio files which are incorporated into this program.

More on that later, but first, let's get this Latin project up and running on your computer.

Installation

If you already have C#2019 installed on your computer you probably know how to run the app but you'll still need to download the DataFiles.  However, if you're a Latin person with little or no computer programming experience, you can still use this app by downloading the precompiled version which is meant to run on Windows 10.  

Newbie on a Windows 10 computer : download the precompiled version here.  When you have this file, decompress it anywhere on your harddrive and find the executable.

C:\ ... whereever you decompiled it ... \Latin Project 2_2\Latin Project 2_2\bin\Debug\Latin 2_2.exe

You'll probably want to create a Windows 'Short Cut' by selecting the file, right click to get a menu and then, under the 'Send to' menu option, click 'Desktop'.  Otherwise you'll have to find the file and double click it everytime to launch it.  (You'll still need to install the Data files ... see instructions below)

If you're not running Windosw10, the first thing you'll need is a working copy of C#2019 (Preview for the moment).  You can download this for free through Microsoft's Visual Studios.  Its a large product and may take a few minutes to install(just make sure you select everything available that has 'C#' listed in it when you install Visual Studio).  Once  you have it running download the first compressed file above and extract its contents anywhere on your harddrive.  Launch C#2019, Load Solution using the file menu options and find the file LatinProject_2_2.sln in the directory where you extracted the contents of the source code file.  Follow the instructions below for all the data and when you're ready, return to C#2019 and press F5.  id agere potestis!

Data Files

With 7-zip

If you have 7-Zip you can download all the Data files using this link to my GoogleDrive.  Decompress this file onto your hard drive.  The app expects to find them at the root of your c:\ drive, so just use 7-Zip 

N.B. the Latin Project source code(the app itself) expects to find these files at the root of your c:\ drive.  Once you've decompressed this file onto your harddrive all should look as it does in the images below with a new directory 

C:\Latin\

if you don't have 7-zip you can get a copy at this link,  otherwise you'll have to download ALL these files listed here below and follow the instructions further down

Without 7-zip

Here's what you do if you do NOT have 7-zip (or if you just want to wrangle it the hard way), you'll need to do is create a sub-directory 'Latin' on your C: hard-drive. This is hard-coded into the Application so there's no getting around it without a bit of pain.  You could go through the code and change all the file references but since they're scattered about in a myriad places I really suggest you stick with this.

N.B. as none of these files have been updated with any of the corrections I've made since 2018 (or so?) you'd be much better served to use the most up to date 7-zip file if at all possible as downloading and installing without it is what most people would call a little bit of a major hassle.

Download c_Latin_Cassells_Dictionary_.zip - 7.3 MB

The image below is a useful guide to getting you started and setting the Latin Project up on your laptop.

There are a lot of files.  And they all need to be extracted into their appropriate directories.  So the first thing you need to do is create the necessary directories.  You should be proficient with Windows Explorer enough to do that.  Most people are, just go to your C: drive and right-click to elicit the context menu, choose New, select Folder and type in the name.  The directories you need to create are shown in the image below 

  1. C:\\Latin\\
  2. c:\\Latin\\Audio\\
  3. c:\\Latin\\Data\\
  4. c:\\Latin\\Data\\CS_Library
  5. c:\\Latin\\Data\\LUT
  6. c:\\Latin\\Library\\

The compressed audio file that you'll want to download is located at Chapter Audio File. Just extract the content of that file into the c:\\Latin\\Audio\\ directory and they'll get processed by the Latin Project the next time you launch the app.  ( edit 2019/04/02 - all new files uploaded most do not have names shown in images below but follow the same nomenclature rules described - text after the first underscore is a brief description of content of .zip file & text before the first underscore is the location where that file needs to be extracted)

edit - 2019/04/11 - the file names in the image below are as they appear on my PC before they are uploaded to CodeProject and have round-brackets - NOTE : the files you will download have their round-brackets replaced by underscore, and may or may not be identical to those shown below.  Just remember that their target path is described in their file-names in the text before the first underscore.

Image 3

once you've created those directories extract the files shown in the diagram above into their respective directories.  You can tell by the name of the file which directory each file belongs.  For example in the orange box at the bottom right of the diagram above(just above these words) the file "c.Latin.Library(biblia + A - C).zip" contains the Library directories A-C and they belong in the "c:\\Latin\\Library\\" directory.  The directory name is written at the beginning of the file's name and the content of the file is inside the round brackets.  It may look scary and a lot of work but I assure you, its worth the trouble.  As deSade was quoted in that movie, "Well worth the dig!"

edit note : 2019/03/18 - I found a Latin error in fero, ferre, tuli, latum (the whole thing needed to be user-defined not just the exceptions because the App didn't realize its base is fere-) and discovered that the Rebuild_LUT() was not recognizing the Future_Active_Participle_Feminine word-type and all 12 of its entries were declared Nominative Singular (e.g. amaturarum => Nominative Singular, Amaturis => nominative singualr, Amaturae =>Nominative singular... you get the idea)

As a consequence to these changes the files with the word LUT in them are not identical to the ones listed in the images above but do follow the same naming rules I explained earlier so this should not be difficult.  Just extract the files named "c.latin.data.lut( --- ).zip" into the "c:\\Latin\\Data\\LUT\\" directory.

edit note : 2019/03/20  - I've been working on a Latin Crossword Puzzle (generator and game) and its finding many(a few) problems that I had never noticed.  The latest discoveries were the words that have an exceptional base.  exeo, -ire, affero, -ferre, for example that had all of their solutions identified as --infinitives-- rather than the appropriate Present Active whatever-it-was-supposed-to-be.  That's fixed.

Also there were 8 records out of 24000 that had a space between the hyphen and the word ending in the headings of those 8 dictionary word files.  This was making it impossible for the opifex to do its job and conjugate/decline properly for those 8 words.

finally - there were a few forms that resulted in problems if they were disposed using the OS form controls(the X at the top of the form when you press shift-alt).  These resulted in problems when they were being called again.  seems to be fixed.  we'll see.

editnot : 2019/04/02 - I've been working on my CrossWordPuzzle and have discovered many files in the dictionary that were not working correctly, specifically verbs.  I have since written a few lines of code that cross references all the verbs in the dictionary.  makes 2 lists : 1) verbs with all four principal parts 2) verbs that lack one or more principal part (excluding irregular verbs, semi-deponent and deponent verbs).  Then it made an alpha-tree of all the complete verbs and ran each of the incomplete verbs through the alpha-tree to find matches.  

To test if an 'incomplete' (missing principal parts) verb was actually a verb composed of a prefix and another 'complete' verb, it searched for the final letters of the incomplete verb in the completed-verb-list.

e.g.
dēmulceo, dēmulcēre, dēmulsi,  -> dēmulceo, dēmulcēre, dēmulsi, dēmulsum

the verb:  "demulceo, demulcere, demulsi" had this text for heading.  Its 4th principal part missing.  The project automatically generates a 4th principal if its missing.  In this case it thinks : "demulcitum" using the rules of latin(as I understand them) and the "habeo, habere, habui, habitum" example that I was working with.  Which is wrong.  But Cassell's dictionary didn't say any better(or i simply neglected to enter the 4th principal part when I typed it) and this word was getting all its perfect-passive forms wrong.

what my 'debug' code did was find these 'incomplete' verbs and run them through the alpha-tree of 'completed-verbs' to find matches.  I could have made a list of all reasonable prefixes and tried to remove them and see if they resulted in 'finds' in the alpha-tree but instead it took the last 4 characters of the 1st principal part and searched the alpha-tree for a match, adding one character after every failure.  When it found a match, (in this case failing with 'lceo' & 'ulceo' and finding 'mulceo') it used the remaining unused letters "de-" as a prefix, added them to the 4th principal part of the matched completed-verb "mulceo, mulcere, mulsi, mulsum" and came up with "demulsum" as the missing 4th principal part.

I made a record of all the changes in this text file :  Verbs Missing Principal Parts if you're interested.

You can see the code to do this in the commented-out debug function

 void findVerbsMissingPrincipalParts()

edit note : 2019/04/11 - no major changes to the code, but I've found still more dictionary entries that needed reworking.  LUT files and Cassells Dictionary files were all updated and provided above.  The following link will provide you with the information reflecting the changes made to the Cassells Dictionary entries Verbs Missing Principal Parts .

edit note : 2019/06/05 - I've been solving a Crossword Puzzle or two per day and have accumulated enough errors in the source material to justify an update - today's update includes new Source-Code, Look-Up Table & Cassell's Dictionary files.  For a detailed look at the most recent changes see Changes_To_LatinProject_20190605.zip - 563

When you're done, your c:\\Latin directory should look like this.

 

Image 4

Now you're all set!

As I mentioned above, the Look-up Table and all the other search engines are good to go.  You won't have to generate them all yourself because the algorithm that generates and uses them has separated them into file sizes that can be uploaded to the Code-Project web-site.  (all those files you're dealing with)  If the thought of downloading everything and extracting them all in their appropriate directories is daunting and you don't think you can do it, I know you can.  Otherwise, download the Cassell's dictionary, Library and audio files then expect to watch and wait for a week for the App to generate all the search engines you need to get it up and running properly.  

A bit of code :

The Search-engines are comprised of Binary trees that have each leaf point to a Linked-List, where the Linked-List elements contain the information used by the app to give the user what she's looking for.  The difference with this version, relative to the original published app, is that they are broken down into smaller segments.  How this is done is relatively simple.  There is a class that manages the file-streams for all the open files.

C++
public class classFileStreamManager
{
    Semaphore sem = new Semaphore(1, 1);
    public FileStream fs;
    public void WaitOne() { sem.WaitOne(); }
    public void Release() { sem.Release(); }
}

This class is the return value of a function that is provided with an record index parameter.  The function allocates a FileStream along with its own Semaphore and positions the FileStream at the correct location for the calling function to read or write to it.  The file is opened once(or created if need be) and remains opened until the app is closed and disposed.  Here below you can see how they work together in a typical example.

 

C++
public static string LUT_BinRec_FileName_Get(int intFileNumber){  return classXmlLatin.strWorkingDirectory + "Data\\LUT\\Latin_LUT_" + intFileNumber.ToString("000") + ".bin"; }

public static int intLUT_Bin_NumRecPerFile = (int)Math.Pow(2, 15);
public static classFileStreamManager LUT_BinRec_FileStream_Get(int index)
{
    int intFileNumber = (int)Math.Floor((float)index / (float)intLUT_Bin_NumRecPerFile);
    int intIndex_Revised = index - intFileNumber * intLUT_Bin_NumRecPerFile;
    classFileStreamManager cFS = null;
    if (lstSemFSLUT_Bin_FS.Count > intFileNumber)
        cFS = lstSemFSLUT_Bin_FS[intFileNumber];

    if (cFS == null)
    {
        string strFilename = LUT_BinRec_FileName_Get(intFileNumber);
        cFS = new classFileStreamManager();
        if (System.IO.File.Exists(strFilename))
            cFS.fs = new FileStream(strFilename, FileMode.Open);
        else
            cFS.fs = new FileStream(strFilename, FileMode.Create);
        if (lstSemFSLUT_Bin_FS.Count <= intFileNumber)
            lstSemFSLUT_Bin_FS.AddRange(new classFileStreamManager[intFileNumber - lstSemFSLUT_Bin_FS.Count+1]);

        lstSemFSLUT_Bin_FS[intFileNumber] = cFS;
    }
    cFS.fs.Position = intIndex_Revised * classLUT.lngLUT_Bin_RecordSize;

    return cFS;
}
public static classLUT_BinTree_Record LUT_BinRec_Load(int index)
{
    string strFilename = classLUT.strLUTBINFilenameAndDirectory;
    classLUT_BinTree_Record cRetVal = new classLUT_BinTree_Record();
    classFileStreamManager cFS = LUT_BinRec_FileStream_Get(index);
    cFS.WaitOne();
    BinaryFormatter formatter = new BinaryFormatter();
    try 
    {
        cRetVal.left = (int)formatter.Deserialize(cFS.fs);
        cRetVal.right = (int)formatter.Deserialize(cFS.fs);
        cRetVal.LL = (int)formatter.Deserialize(cFS.fs);
        cRetVal.strWord = (string)formatter.Deserialize(cFS.fs);
    }
    catch (Exception e)
    {
        classDebug.instance.Print_Exception(e);
    }
    cFS.Release();
            
    cRetVal.intMyIndex = index;

    return cRetVal;
}

The function LUT_BinRec_Load(int index) needs a FileStream from the LUT_BinRec_FileStream_Get() 

  1. the Filename is derived from the index by dividing the index by the number of records per file
  2. then it opens a new FileStream if it isn't already opened
  3. it calculates the record's location and positions the FileStream where it needs to be
  4. sends the FileStream & its own Semaphore together to the calling function

you'll notice that the number of records per file is a multiple of 2 Math.Pow(2, 15).  Even though the result may be only over 5Megabytes, doubling it to Math.Pow(2,16) would exceed the 10Megabyte threshold of this site.  A multiple of 2, of course, in binary is a nice round number the computer can Right-Shift to good use without having to do long tedious tiring and aggonizingly slow long-division.

The LUT_BinRec_Load() 

  1. function receives its FileStream, trusts that its where it needs to be
  2. Waits for the semaphore to give it a green light
  3. loads the record from the FileStream
  4. Releases the semaphore and quits

The added bonus of this is that now if multiple threads are searching via the same engine they rarely run into each other because the odds are they're reading different files, and whenever they do conflict the semaphore is there to prevent any major fender-benders or app-crashing collisions.  Keeping the FileStreams opened throughout the life of the App speeds up the searches and they're quick and reliable.  

Latina est Gaudium

Yes, Latin is a joy! But first, you'll have to pick up on the language because this isn't pig-latin we're talkin' you know! The language is old, and for all intents and purposes, currently considered dead, but that's no reason to kick it when it's down. Let me tell you a bit about Latin and why this program is more than just an electronic text-book. Nouns all have a gender, including a third gender called 'neuter', which means 'neither' in Latin (bet you didn't know that!). Then each noun and adjective has six cases and two numbers (singular and plural). What's more is adjectives have three different types of ways to be used positive, comparative, and superlative; all adjectives have three genders, all have two numbers, and all have six cases:

Image 5

3 (genders) x 3 (types) x 2 (numbers) x 6 cases = 108 different ways to spell the same adjective.

Every adjective.

And verbs?!? Between passive and active, plural and singular, you have the persons first, second, and third. Present, past, and future, subjunctive, participles, infinitives... there's a lot! And they're all very similar, but different one from the next. And that's if you figure out if the verb is first conjugation, second, or third! Or maybe, it's a third IO. Then there's this scary thing called the 'passive periphrastic'.

In short, what I'm saying is, if you're trying to learn Latin with nothing but a textbook, you're going to run into some trouble. And that's where this program comes in.  It will conjugate and decline every word in the dictionary and provide you with all the alternate spellings of every word.

Take, for example, the word "fero, ferre, tuli, latum".  These are the four principal parts of the verb "to bear".  So if you're reading something and you don't know what tulissent means you go to the dictionary on your shelf and look under the letter 'T' and never find the 3rd Person plural spelling of the Pluperfect Subjunctive of the verb fero, ferre.   ... Unless, of course, you've got this project up and running.  Then you're in the know.  

Opifex: Skilled worker

When you first launch the program, you'll be looking at the front cover to Wheelock's Latin. The gray underlined-type print beneath the image are all links to other parts of the book. The textbook part of the program works in a way similar to HTML and the internet, except that it's a network of files that are all contained in a single directory on your hard-drive which you can navigate through. This is done using a class called xmlRecord, in combination with classGraphics, which I talked about in GCIDE: A Complete English Language Dictionary.

classXmlLatin is similar, but does not have the links between files which join the network of files that make up the textbook. This is because the dictionary files which it handles do not require any network, and are loaded exclusively using references from the Look-Up Table. Remember the LUT mentioned earlier? This is where the Latin actually happens. classLatin relies on classXmlLatin to retrieve the dictionary entries which it uses to create the look-up table.

When you finally get around to letting the program build you your LUT, you'll find that the program will be very MIPs needy while in the process of building this database. It will keep your computer particularly busy for nearly 24 hours! You can stop and restart it any time you like at no extra cost, and you may want to just let it work overnight. That seems like a long time, but if you're downloading this program because you need (or want) to learn Latin, then you won't want to go without it.

classLatin uses an opifex, or skilled worker, to do all the work. The program only needs to instantiate one classLatin; this class then creates itself a resource handler as well as that famous opifex on which it relies. The resource handler manages classWordInfo. These word-info objects hold all the information from the dictionary entries which the opifex requires to conjugate or decline, whatever the case may be. classWordInfo also has two combo boxes which can be put on the screen if need be. Any number of these can be requested from the resource-manager, and whenever the user makes a selection change on either combo boxes, classWordInfo will pass itself over to the opifex to deliver the Latin.

The actual code for classOpifex is not altogether very complicated because all the work is divided into a multitude of functions that are named with their specific tasks in mind. For most conjugation/declension functions, the opifex receives as parameter an instance of a classWordInfo. Since each function is written for one exclusive purpose, that's all any of these functions need to do what they do. In the original Visual Basic version of this program, I made the mistake of trying to have one function do everything, and discovered that that created new problems with every new chapter of the textbook. This C# version doesn't have any one function with an "I can do it all" attitude, but rather relies on a small army of functions that each handle a different Latin conjugation or declension.

Here's an example ....

C#
public classLatin_C.classSolution conjugate_Present_Indicative_Active(classWordInfo word)
{
    if (verbIsDeponent(word))
        return conjugate_Present_Indicative_Active_Deponent(word);

    classLatin_C.classSolution cRetVal = new classLatin_C.classSolution();
    string[,] strSolution = new string[2, 3];
    cRetVal.strSolution = strSolution;
    cRetVal.typeSolution = getTypeSolution(classEnum.enuVerbTenses.Present_Indicative_Active);
    // 1st, 2nd, 3rd, 4th, 3rd-IO 
    string[,,] strEndings = {{{"ō", "ās","at"}, {"āmus","ātis","ant"}},
                        {{"eō", "ēs","et"}, {"ēmus","ētis","ent"}},
                        {{"ō", "is","it"}, {"imus","itis","unt"}},
                        {{"ō", "s","t"}, {"mus","tis","unt"}},
                        {{"iō", "is","it"}, {"imus","itis","iunt"}}};

    string strBase = word.strWords[1];
    if (word.eWordType == classEnum.enuWordType.verb_4th_conjugation)
        strBase = word.strWords[0].Substring(0, word.strWords[0].Length - 2) 
                  + makeLongVowel(word.strWords[0][word.strWords[0].Length - 2]);
    else
        strBase = strBase.Substring(0, strBase.Length - "are".Length);
    classEnum.enuVerbConjugations verbConjugation = getVerbConjugationFromeWordType(word.eWordType);

    for (classEnum.enuNumber numberCounter = classEnum.enuNumber.Singular;
                                numberCounter <= classEnum.enuNumber.Plural;
                                numberCounter++)
        for (classEnum.enuPerson personCounter = classEnum.enuPerson.First;
                                    personCounter <= classEnum.enuPerson.Third;
                                    personCounter++)
            cRetVal.strSolution[(int)numberCounter, (int)personCounter] 
                   = shortenLongVowels(strBase + strEndings[(int)verbConjugation, 
                                       (int)numberCounter, 
                                       (int)personCounter]);
    cRetVal.strRowNames = getRowNamesOfConjugation();
    cRetVal.strColumnNames = getColumnNamesOfDeclension();

    return cRetVal;
}

In this example, the opifex is being requested to conjugate the word it is provided in the Present Active Indicative. It will first ask "is this a deponent verb?", and if so, branch off to the opifex function which will take care of it if it is. Otherwise, it creates an instance of a structure called udtSolution and translate the verb tense into a typeSolution (two enumerated types). Then, because the Latin rule for this case is to chop off the last letters of the verb's second participle (strWord[1] in classWordInfo) before adding the personal endings, it generates a variable called strBase and proceeds to add the appropriate endings, fetches the row-names and column-names for this type of solution, and wires it all back to the calling function.

If you take a closer look at these functions, you'll find that they all pretty much do the same thing, with only minor variations for their specific tasks. Because there are so many ways to conjugate and decline nouns, adjectives, and verbs in Latin, it was just easier to do things this way than to worry about saving code-space by consolidating them all into one, or even only just a few functions. This design makes debugging much easier. This way, if next year I find that I've been declining third declension feminine superlative adjectives incorrectly, there's only one function to fix, and it won't affect anything else when I debug it.

Bada-bing, bada-boom.

In combo two

Whenever a dictionary entry appears on the screen, you'll find it comes with a few extra features besides the definition. Looking at the two combo-boxes above the word's entry, you'll see that the one on the left tells you what kind of word you're looking at, e.g., magnus, -a, -um: first declension adjective. Listed below this, you would see the three genders for each of the three ways this adjective can be declined: positive, comparative, and superlative. You can choose any one of these entries and get an output. Selecting any specific gender will give you the two numbers and 6 case spellings for this adjective for the gender/type you've chosen. Clicking on the Comparative heading, somewhere near the middle of the list, will produce another form with all 3 genders x 2 numbers x 6 cases (all the comparative spellings of magnus!), and if you're still not sure what you're looking for, the top most heading with the word's type-info I mentioned earlier will spit out all 108 ways this particular adjective can be spelled onto the screen in one clean sweep.

Image 6

notice the difference between the above image and the one below.  If you didn't immediately notice, the bottom image has several extra features that the one above does not.  This is because the bottom image is a screen capture of the same word's Dictionary Definition form taken when the user has logged in as Administrator.  Because the Dictionary is so easily edited, to prevent anyone from tampering too easily with your data and tinkering with your Latin dictionary or other files, which only a responsible user should do, you now have a login-feature.  more on this later.

Image 7

Verbs and nouns are similar, but with tenses, passive and active, or in the case of nouns, just the same as adjectives, but only for the one gender. Just play around with it, and you'll see what I mean.

And in the right corner!

The other combo-box is something I call Latin logic. This is loaded with links to the Wheelock's Latin textbook's pages that are specifically related for the particular word you're looking at. You might be curious to know what a third conjugation-IO verb is when you popup a dictionary form with the definition for the word capio, -ere, and scanning over the list of entries there, you'll find exactly what you're looking for. Then, when you select an entry from the Latin Logic combobox, a whole new instance of your Latin textbook pops up on exactly the right page, chapter 10, sub-chapter "IO verbs", for you to read at your delight.

There's a LUT to do

You really should have your program build you a Look-up Table because without it, you're missing out on a lut!(sic). Let me explain. If you haven't been able to actually see a dictionary entry and tell the opifex to get to work for you, that's because you haven't yet got your look-up table on-line. Once you do, it's a wiz. What the look-up table does is very similar to what the GCIDE article mentioned above does: it helps you find what you're looking for. It tells you what a word is, how many ways that particular spelling can be derived, and where to find the root of the word you're looking at. To do this, the program goes over every entry in the Latin/English dictionary, it loads each word-entry one at a time, generates every possible spelling it knows for all of these words, then stores them into a database along with the means by which each spelling was obtained. Then, when the user asks it what the word capiunt is, for example, it scans the database and tells you that capiunt is the word you would have found in your pocket Latin dictionary listed under capio, -ere, cepi, captum had you picked it up off the shelf; however, the LUT will also tell you that it's conjugated in the Present Active Indicative, third person, plural, which you would otherwise have to figure out for yourself.

Don't think you'll get away with not memorizing your conjugations and declensions and still somehow become proficient in Latin.  That is sadly incorrect.  You still have to put the time in to actually know how to conjugate and decline, what this app will do will help you make sure you're doing it right.  Just don't let it make  you lazy.

Latin_textbook/LUT__hard__at_work.png

The image above shows you the textbook page 'On a Temperamental Friend' at the top. Clicking on the word difficilis which appears in the first Latin sentence shows you the difficilis form which lists all the possible ways this particular spelling can be derived, and selecting the first one pops up the next form in front of it (dictionary entry with its two combo-boxes) or the blue form in front of that one (positive masculine declension) by clicking either the LEFT-Half or the RIGHT-Half of the list of possible ways this particular spelling can be derived, respectively.   Click on the LEFT-half and you call up a DictionaryDefinitionForm which lets you then choose the Latin-Solution you want, or click on the RIGHT-half and you get the solution item you clicked from the list.

You'll notice that this word also shows the "audio" button, which when pressed plays an audio recording of this word and how it is pronounced (that is, if you've downloaded the audio files). Notice the blue? Nouns and adjectives are color coordinated for their genders.  Blue for Masculine and pink for Feminine nouns.  Adjectives are displayed in the same three colors but where nouns have only one specific gender, they have only one specific color each and Adjectives (matching the nouns they modify by number, case & gender) will show all three.

Latin is particular because had you been looking for the word "ceperant" in your pocket dictionary, you'd probably not think to look under "capio" and may or may not find it. But with this LUT, all that is done for you! Unfortunately, so is most of the thinking, which makes it very easy to rely entirely on the program and not do any actual Latin-learning at all, as I have so happily discovered.  It took me years to actually learn these, I'm a bit slow.  They must be learned, if you're serious about any language the rules of conjugation/declension are critical.  So sit down, exercise your brain and learn them.

The LUT first gives you a list of ways the word can be derived. From there, you can ask it to give you the dictionary form of the entry you've selected, or to just put the solution to the screen straightaway. You can do either of these by clicking F1 for one or F3 for the other when there is only one item in the list. Alternately, you can click the left half of the form or the right, and a pop-up helpful text tells you which you're about to select.  'Click here for full derivation' will appear in the pop-up text when your mouse cursor is on the left-half and 'click here for specific derivation' will appear in the pop-up text when your mouse cursor is on the right-half.

The program's mode selection allows you to choose between workbook, textbook, and vocabulary. textbook is the default start-up value, and this is where you click on words, links, or images. Clicking on a Latin word will cause the program to search the LUT for the word you clicked, making reading Latin just a little bit less daunting for any newbie. If you happen to click a word it doesn't find in the LUT, the program won't scowl or argue, but rather it'll kick back and wait for your next request. The PictureBox's mouse-click event in the form classGraphicOutputPanel below shows you where:

C#
 void pic_MouseClick(object sender, MouseEventArgs e)
{
    if (DisableNavigation
        && ((wordUnderMouse != null 
            && wordUnderMouse.eWordImageType != classEnum.enuXmlRec_WordImageType.solution)
        || (wordUnderMouse == null)))
        return;

    if (wordUnderMouse != null)
    {
        switch (wordUnderMouse.eWordImageType)
        {
            case classEnum.enuXmlRec_WordImageType.link:
                string strFilenameAndDirectory 
                      = cLibXmlRecord.strWorkingDirectory 
                        + wordUnderMouse.strAddress + ".xml";
                if (System.IO.File.Exists(strFilenameAndDirectory))
                {
                    loadFile(wordUnderMouse.strAddress);
                    cutNavigation();
                    addNavigationPage();
                }
                else
                { // exit
                    if (DisableNavigation)
                        return;
                }
                break;

            case classEnum.enuXmlRec_WordImageType.solution:
                frmPopUp.rtx.Font = new Font("ms sans serif", 12, FontStyle.Regular);
                frmPopUp.rtx.Text = wordUnderMouse.strAddress;
                FormPopup_PlaceAndShow();
                break;

            case classEnum.enuXmlRec_WordImageType.image:
                Form frmPic = new Form();
                PictureBox pic = new PictureBox();
                frmPic.Controls.Add(pic);
                pic.SizeMode = PictureBoxSizeMode.AutoSize;
                pic.Image = wordUnderMouse.bmp;
                frmPic.Size = new Size(pic.Width + 15, pic.Height + 35);
                frmPic.Text = wordUnderMouse.strText;
                pic.Location = new Point(0, 0);
                frmPic.KeyDown += new KeyEventHandler(cLibLatin.frm_KeyDown);
                frmPic.Show();
                break;

            case classEnum.enuXmlRec_WordImageType.plain:
                classLUT.LUT_Search(classStringLibrary.RemoveMacrons(wordUnderMouse.strText), 
                                    classEnum.enuLUT_Search_Output.frmResultShow, 
                                    classEnum.enuLUT_Search_CallingFunction.notRelevant);
                break;
        }
    }
}

The textbook is nothing more than a textbox with one extra feature; you guessed it, it is a Latin-LUT friendly zone. Just click the word you've typed and press F1, F2, F3 or F4.

  • F1 - LUT - Look-Up-Table
  • F2 - Dictionary - gives you an alphabetical list of words in the dictionary
  • F3 - Dictionary Content Search - searches the definitions of the dictionary
  • F4 - Library search - searches the ancient Latin texts stored in your library sub-directory

You can right-click to get a context menu and see these options listed there.

Presenting the lovely Look-Up Table Panel

The left of the main form includes a hidden panel for your LUT controls.

Image 9

Aside from giving credit to this program's source material, it offers you the option of using the Latin-LUT in any other program by enabling a timer which tests the contents of Microsoft's clipboard for text. Whenever it finds text there that wasn't there the previous time it checked, it'll clip off the first word and plug it into the look-up table. This is convenient when you're reading screen-plays of Supernatural or researching the Vatican's Documenta Lingua Latina for a good exorcism, or if you're just curious to know if that familiar brand-name you've trusted all these years is actually a Latin epithet in disguise. To use it, when you've checked the checkbox on the LUT-panel, you need only select the text you want to run through the LUT, i.e., on your web-browser, and copy it into the Operating-System's clipboard by highlighting it and then pressing the key-combination Ctrl-C.

 

Search panel

Image 10

Latin-wise, sometimes you need to search your textbook for something you may have missed. And that's what the panel at the bottom of your screen does. Selecting the Heading checkbox searches every chapter title and sub-chapter title from your Wheelock's textbook for the words you're looking for. Glossary searches only the glossary words and their English definitions. This is convenient when you're trying to translate from English to Latin, its similar to the Dictionary Content Search but limited to the glossary. If you want to know the words for "girl" in Latin, you can search for it in the glossary or the Dictionary Content and the results will give you the glossary word puella, -ae which has the word girl in its definition. From there, you can click on the word puella and have the look-up table spill out the 12-part declension in pretty pink! Not checking either of these check-boxes will search everything in the textbook apart from the glossary entries.

Code-wise, you may find the panel's buttons interesting. The background image is stored in the program's resources and then copied onto the panel at run-time. Then, each button/checkbox is placed onto the panel, and for each one, a bitmap is created from the same source image and placed strategically onto that object so as to complete the picture. This makes for a nice graphic effect.

As for the actual code which does the searching, it's got some similarities to the LUT in that it relies on binary-stream files with records of identical size which can be retrieved and saved in a random access fashion. These records each contain a word that can be searched, along with the file in which it can be found and three integer variables used to create a binary-tree/linked list that make up the database. Whereas the LUT uses two separate file types (multiples of each to fit the Code-Project's upload 10MB criteria), one for the binary tree and the other to hold all the linked list entries which each leaf in the binary tree points to, the textbook search engine has all the information on a single file. Or three, rather, since there are three different types of searches: normal, glossary, and heading, which each "require" their own file. I say "require" in quotations, but if the three different trees had their root nodes at records 1, 2, and 3, respectively, then all three search engines could be stored in the same file. You would only need to know how many trees you want in the file and reserve those three entries for the root-nodes of each tree. Putting that idea aside, this program uses three different files for the three different search modes. There's only one actual search-engine, and since the getRecord() and putRecord() functions get a Mode parameter, they can open and close the appropriate file at every execution. Mode is an enumerated type, and the three file names are stored in an array with the enumerated type's integer values in mind so that getting the right file is as easy as indexing it with the mode type.

C#
classSearchRec_FileTreeElement getRecord(long index, enuTypesSearches Mode)
{            
    FileStream fs;
    if (index < 0)
        return null;
    if (System.IO.File.Exists(strSearchBinFilenameAndDirectory[(int)Mode]))
    {
        fs = new FileStream(strSearchBinFilenameAndDirectory[(int)Mode], FileMode.Open);
        fs.Position = getBinPosition(index);

        BinaryFormatter formatter = new BinaryFormatter();
        string strFilename = (string)formatter.Deserialize(fs);
        string strWord = (string)formatter.Deserialize(fs);

        classSearchRec_FileTreeElement cRetVal = 
           new classSearchRec_FileTreeElement(strWord, strFilename);

        cRetVal.lngLeft = (long)formatter.Deserialize(fs);
        cRetVal.lngRight = (long)formatter.Deserialize(fs);
        cRetVal.lngNext = (long)formatter.Deserialize(fs);

        cRetVal.intSizeLinkedList = (int)formatter.Deserialize(fs);
        fs.Close();
        return cRetVal;
    }
    return null;
}

You can see in the second if statement above where the IO system tests the existence of the search-engine's file which is stored in the strSearchBinFilenameAndDirectory[] array indexed by the integer value of the input parameter Mode.

C#
public class classSearchRec_FileTreeElement
{
    public long lngLeft;
    public long lngRight;
    public long lngNext;
    public string strWord;
    public string strFilename;
    public int intSizeLinkedList;

    public classSearchRec_FileTreeElement(string Word, string filename)
    {
        lngLeft = -1;
        lngRight = -1;
        lngNext = -1;
        intSizeLinkedList = 1;
        strWord = Word;
        strFilename = filename;
    }
}

A more interesting part of the search engine may be the way it does the boolean searches. Whenever the user types more than one word into the search engine, the program needs to find the results and put them to the screen. To understand how this works, you first have to know that when the search-engine's database was created, every file was scanned, every word in every file was entered into the appropriate databases, and no doubles of any one word from any one file made their way into the database. So even if the word "the" appears twelve times in any one particular file, it is entered into the database as having been found in that file "at least once" and no more. Also, whenever multiple files have the same word, the binary tree has to branch out at that particular leaf into a linked list (all in the same file, using the same record type which has all three integer indices for the binary tree links intLeft and intRight as well as the linked-list intNext). Whenever inserting new finds into the binary-tree leaf's linked-list, the leaf of the tree (as opposed to a node in the linked list it points to) keeps track of the number of entries in the linked list. This amounts to keeping track of how many files have this particular word in them without having to count the number of entries in the linked list. Since the search engine need only be built once and is then only referenced and is never edited, storing this value in the first element of the list makes it readily available.

And so, when the search engine finds itself having to cross reference two or more lists of filenames to see if any of them contain both of the words you're comparing, it does this by first generating a binary tree in RAM memory of the shortest list, then runs every entry from the next shortest list into this binary tree. Filenames contained in the second list which are also found in the first list's bin-tree of filenames then go into another bin-tree, which is subsequently used for the third word's file list. The code keeps an array of two pointers to the roots of these RAM trees, and alternates between the two, scanning bigger and bigger lists through smaller and smaller trees, until it eventually scans an sum of all results-lists without finding a single filename in the previous tree, at which point, it quits, or it has scanned all the lists and now holds the names of all the files which contain every search word the user is looking for in the last created RAM bin-tree.

C#
public class classSearchRec_RAMTReeElement
{
    public classSearchRec_RAMTReeElement Left;
    public classSearchRec_RAMTReeElement Right;
    public classSearchRec_RAMTReeElement Next;
    public string strWord;
    public string strFilename;
    public int intSizeLinkedList;

    public classSearchRec_RAMTReeElement(string Word, string filename)
    {
        Left = null ;
        Right = null ;
        Next = null;
        intSizeLinkedList = 1;
        strWord = Word;
        strFilename = filename;
    }
}

All this is done in classWheelockTextbook_Search's

C#
public string[] searchWord(string strSearchWords, enuTypesSearches eMode)

Pop-up form

The button seen in the image below at the top right of the form with the ^ on it produces a new pop-up form loaded with the current page and has all the features of the app's main form. Its "home" navigation button will return you to the file it was loaded with, and so if you get lost stomping the grounds, you can always find your way.

Panels and modes

Image 11

In the image above you can see some of the Navigation controls have changed since the previous article was published nearly ten years ago.  The combobox is still the same, it keeps track of your most recent visits across the Wheelock's Latin textbook.  To the right you can see the Home icon which takes you back to the 'home-page', which displays the front cover of the book and lists all the chapters.  The Left and Right arrows(Left arrow only is visible in example) can take you back or forward in your recent readings, while the double-square icon is replicator type device that will popup an exact copy of the record you're looking at should you want to see several pages simultaneously.

 

Image 12

 

In the image above, you can see the four different side-panels which appear on the screen. I call them side-panels because the tabs that stick out along the left edge of the form can be clicked to bring them to the front, and that's what they look like when all four of them have been brought forward in that way. When you click on them again, all you see are the words on the left edge sticking out the side, thus the name. I've already talked about the search panel and the LUT vanity-panel, but you haven't heard about the other one labeled Mode and ToolTip. No mystery here, and it may not need be said, but the Mode panel is where you select what mode you want the app to be in and the ToolTip panel toggles the tool tips.

Vocabulary mode

From the mode-panel, you can select either of the three modes: Textbook, Workbook, and Vocabulary mode. Clicking on the Vocabulary radio-button brings you here:

Latin_textbook/vocabulary_mode.png

This is the Vocabulary mode. You can review your Latin-vocabulary using flash-cards on your computer. classVocabulary is the groupbox you see in the above image with the word Vocabulary written on it. The combo-box on the right has a list of all of Wheelock's Vocabulary Chapter files (forty chapters), and you can select or unselect from which chapters you want your flashcards to be chosen at random the next time you review your Latin vocabulary. Whenever you select a chapter, all the words contained in that chapter are added to a running list of words to review. Then, when you click the button Review Words beneath the combo-box, a number of new words are randomly picked out of your selected review chapters and added to your current list of vocabulary words before the flash card appears with the first word for you to study.

Latin_textbook/flash-card_front.png

The image above shows you the front of the flash card. The word appears in Latin; in this case, the Latin verb cogito with its four principal parts. Moving the mouse around, you'll find that the three words near the top, "Delete", "Next", and "Quit", are click-activated and work like buttons. But first, you'll want to flip the card over. To do this, you grab the image anywhere that's not a button and move the mouse vertically while holding down the mouse-button. This will animate a flipping motion, which allows you to display either side of the flash-card depending on the position of the mouse.

The flipping of the card illusion is a simple trick. There are two picture boxes in stretch-size mode, fill-docked to the form, one on top of the other. The program keeps track of which side you're looking at with one variable, and remembers which side you grabbed with another, so that when the mouse is moved, the image is contracted and expanded depending on where it was grabbed, above or below the middle of the screen, and where the mouse cursor is at the moment, also relative to the middle of the screen, then decides which image belongs in front of the other. This is all done in the MouseMove event of classFlashCard (in the classVocabulary.cs file).

Notice the boolean global variable bolIgnoreMouseMove. Because all the action happens inside the mouse-move event handler which will move the form, affect the image beneath the mouse, and cause more events to handle, those events must be ignored until it's done doing what it's got to do, or they will cascade into disaster. This boolean is tested before it is set at the start of the event-handler, and then it is reset before exiting, allowing the handler to cause more events in the process of updating the form, certain that those events will be ignored until it's done.

C#
void pic_MouseMove(object sender, MouseEventArgs e)
{
    if (bolIgnoreMouseMove)
        return;
    bolIgnoreMouseMove = true;

    if (bolGrab)
    {
        Point ptMousePosition = new Point(MousePosition.X, MousePosition.Y);
        if (distanceBetweenTwoPoints(ptMousePosition, ptOldMousePosition) > 5)
        {
            ptOldMousePosition = ptMousePosition;

            int intNewHeight = 
                Latin_Project.Properties.Resources.flashcard_source_back.Height;
            int intNewTop = intMidScreen;
            int intGrabDist_Abs = Math.Abs(intGrabDistFromCenter);
            int intMidHeight = Height / 2;
            int intDistanceFromCenter = Math.Abs(e.Y - intMidHeight);

            bool bolSetToNewValues = false;

            if (intGrabDistFromCenter < 0)
            { // grabbed it above mid-point
                if (e.Y > intMidHeight - intGrabDist_Abs)
                { // in the process of flipping or completed action
                    if (e.Y > intMidHeight)
                    { // seeing the side not-grabbed
                        eVisibleSide = otherSide(eGrabSide);

                        if (e.Y < intMidHeight + intGrabDist_Abs)
                        {
                          // fraction of size (still flipping)
                          intNewHeight = (int)(((double)intDistanceFromCenter / 
                           (double)intGrabDist_Abs) * 
                           Latin_Project.Properties.Resources.flashcard_source_back.Height);
                        }
                        bolSetToNewValues = true;
                    }
                    else
                    { // still seeing the grabbed-side
                        eVisibleSide = eGrabSide;

                        if (e.Y > intMidHeight - intGrabDist_Abs)
                        {
                          // fraction of size (still flipping)
                          intNewHeight = (int)(((double)intDistanceFromCenter / 
                           (double)intGrabDist_Abs) * 
                           Latin_Project.Properties.Resources.flashcard_source_back.Height);
                        }
                        bolSetToNewValues = true;
                    }
                }
            }
            else
            {
                // grabbed below mid-point -> (intGrabDistFromCenter > 0)
                if (e.Y < intMidHeight)
                { // seeing the side not-grabbed
                    eVisibleSide = otherSide(eGrabSide);

                    if (e.Y > intMidHeight - intGrabDist_Abs)
                    {
                      // fraction of size (still flipping)
                      intNewHeight = (int)(((double)intDistanceFromCenter / 
                       (double)intGrabDist_Abs) * 
                       Latin_Project.Properties.Resources.flashcard_source_back.Height);
                    }
                    bolSetToNewValues = true;
                }
                else
                {
                   // still seeing the grabbed-side
                    eVisibleSide = eGrabSide;

                    if (e.Y < intMidHeight + intGrabDist_Abs)
                    {
                      // fraction of size (still flipping)
                      intNewHeight = (int)(((double)intDistanceFromCenter / 
                       (double)intGrabDist_Abs) * 
                       Latin_Project.Properties.Resources.flashcard_source_back.Height);
                    }
                    bolSetToNewValues = true;
                }
            }
            if (bolSetToNewValues)
            {
                Height = intNewHeight;
                Top = intMidScreen - intNewHeight / 2;
            }

            pic[(int)eVisibleSide].BringToFront();
        }
    }
    else
    {
        cWordUnderMouse = mainForm.cLibGrText.getWordUnderMouse(
           new Point(e.X, e.Y), uWords[(int)eVisibleSide].cWord);

        if (cWordUnderMouse != null && 
                  cWordUnderMouse.eWordImageType == enuWordImageType.link)
            Cursor = Cursors.Hand;
        else
            Cursor = curNormal;
    }

    bolIgnoreMouseMove = false;
}

Latin_textbook/flash-card_back.png

cogito ergo sum...

The numbers on the top left tell you how many flash cards are still left in this review session and the number of flashcards still left for the next. Clicking on the "Delete" option discards the one you're looking at so that it won't automatically be included in the bunch the next time you review your Latin vocabulary. Alternately, clicking the "Next" option replaces the card you're holding back in the review pile, and another one is randomly picked from the lot.

The New Stuff

Now we get to a few of the new features that were not included in the original article.

  1. Administrator Login
  2. Dictionary Definition Editor
  3. Dictionary Content Search
  4. Dictionary List
  5. Library & Library Search
  6. Tool-tips

Administrator Login

To keep the grubby little hands of any irresponsible user (or student) away from your Look-Up Table and myriad data-trees, many of the features have been disabled for any user that is not logged in as the Administrator.  To login as Administrator you go into the Workbook Mode.  Then click on the File-Menu option and select Login Administrator.  You will then see the form shown below : 

Image 16

The default password is an empty-string, so just press enter the first time you use it and it will log you in as Administrator.  It will then prompt you to change your password, here below :

Image 17

and  you're done.

The Code : 

Once the decision was made to include these Administrator/User access levels it was relatively easy to allow/disallow the many features they control by either hiding or showing the controls that enable the user to interact with the restricted functions.  Once that was done, I took twenty minutes to write the Encrypt/Decrypt functions shown below :

C++
public static string Encrypt(string strText, string strKey)
{
    string strRetVal = "";

    if (strKey.Length == 0)
    {
        strRetVal = strText;
    }
    else
    {
        for (int intEscapeCounter = 0; intEscapeCounter < lstEscapeChar.Count; intEscapeCounter++)
        {
            classEscapeChar cEscape = lstEscapeChar[intEscapeCounter];
            strText = strText.Replace(cEscape.strSource, cEscape.strTarget);
        }

        for (int intChar = 0; intChar < strText.Length; intChar++)
        {
            char chrSource = strText[intChar];
            int intSource = conValidChar.IndexOf(chrSource);

            char chrKey = strKey[intChar % strKey.Length];
            int intKey = conValidChar.IndexOf(chrKey);

            char chrTarget = conValidChar[ (intKey + intSource) % conValidChar.Length];

            strRetVal += chrTarget.ToString();
        }
    }
    return strRetVal;
}

public static string Decrypt(string strText, string strKey)
{
    string strRetVal = "";
    if (strKey.Length ==0)
    {
        strRetVal = strText;
    }
    else
    {
        for (int intChar = 0; intChar < strText.Length; intChar++)
        {
            char chrSource = strText[intChar];
            int intSource = conValidChar.IndexOf(chrSource);

            char chrKey = strKey[intChar % strKey.Length];
            int intKey = conValidChar.IndexOf(chrKey);

            char chrTarget
                  = conValidChar[(intSource - intKey + conValidChar.Length) % conValidChar.Length];
            strRetVal += chrTarget.ToString();
        }

        for (int intEscapeCounter = 0; intEscapeCounter < lstEscapeChar.Count; intEscapeCounter++)
        {
            classEscapeChar cEscape = lstEscapeChar[intEscapeCounter];
                strRetVal = strRetVal.Replace(cEscape.strTarget , cEscape.strSource);
        }
    }
        return strRetVal;
}

What these two functions basically do is they take each character in the sourceText one character at a time and matching each of the characters with one character from the keyText (password).  The list of allowable characters is searched and the index values of both these characters are added together then their sum is reduced to fit the length of the list of allowable characters by using the '%' modulo mathematical function(remember 4th grade where 22 divided by 3 was not 7.33333 but 7 remainder 1?  Modulo is the "remainder 1" part).  The text is first cleansed of its nasty control functions like '\t' and '\r\n', these characters make a mess of the encrypted result so they're replaced with alternate strings.  The altered de-EscapeCharacter-ized text is encrypted the to decrypt the text the same thing occurs in reverse : each character in the encrypted string has its index subtract the corresponding password character's index value to obtain the original character in the de-EscapeCharacter-ized text.  Once the entire text has been decrypted the text is then scoured for the alternate-strings that originally replaced the control characters and the original document is restored.

For the Administrator password, however, there is a file 'hidden' in plain sight called :

Image 18

noone should think much of it and it won't appear until after you first click on the Login Administrator File-menu option.  When you do that for the first time, before you change your password, that file will contain the unencrypted test-sentence that your password will then encrypt and will need to be decrypted with the same password in order to access the Administrator's functions.  Look at it before you change your password and look at it again afterwards, you'll see the difference.  If you ever lose or forget your password, you can simply delete this file to reset it to the default empty string.  This may sound like a useless way to encrypt your password since anybody can read this article and delete that file to access the Administrator functions, but if you are the Administrator of your own computer or computer lab, you can set the security features of this file and all the sensitive directories and their files to 'Read-Only' for all users except you, the administrator.  So, its not infallible, any cryptologist will take twenty minutes to byte through your password.  Its not fail-safe but then it only took me twenty minutes to write it, so it should be good-enough.

Dictionary Definition Editor

As promised, there is a way to edit and create new dictionary entries.  If you discover that the LUT does not include a word you're looking for, you can create an entry for it and the App will generate all the various cases/conjugations for that word to be included into the LUT.  The interface is quite easy to manage.  Have a look at the image below.  It shows you the new menu option available to the Administrator. 

One thing that should be mentioned, another feature, is that you can type macrons very easily by entering a 'prefix' character to the vowel you want to type with a macron.  Just type '/e' and the '/' will dissappear leaving you with ē.  This is also true for the workbook where you can do your writing and use the Spell-checker.

Image 19

Click on Entry Editor and you will summon the Dictionary Entry Editor form shown below.  The file-menu options are standard and shouldn't give you any trouble but if you want to see the results of what you've written you can save the file and click the Options-View menu selection.  This will display the word's Dictionary Definition form, your final output, which you can then test-run and see if it conjugates/declines like you want it to.  If it doesn't, its probably because you mis-identified the word type.  You can select any of the word-type options available to you from the Word-Type combo-box shown below.  The text-files are as straight forward as the file-menu options but you need to consider that this is a Dictionary word and the word you enter must look like what it purports to be.  So look at some examples of similar words that are already in the LUT and its dictionary to see how the headings are supposed to appear.  When in doubt about alternate Heading spellings (here below fustis has one alternate spelling '-is' for its genitive singular) just type the word in full.  If you feel brave enough to edit the text-file that tells the code how to add these endings you're welcome to it (LatinEndings.txt) but I wouldn't advise it because it is very carefully crafted and temperamental, easily made a mess of. 

The Auto-AppendLUT option is available to you if you're sure that the word you've entered will do what you expect it to.  I would recommend you leave this un-checked until you've familiarized yourself with the Editor enough to be sure that you won't generate incorrect cases by mis-identifying the word you're entering.  You can always append the LUT by pressing the AppendLUT button on the Dictionary Definition form for this word.  So keep the Auto-AppendLUT option un-checked, view your result, test it and then, if you're happy with it, Append it to LUT.  You can append the same word to the LUT as often as you like but doing so will mean that its searcch results will be appear as many times as you've done so.  It may not be much but it does eventually become a problem necessitating drastic measure.

Pressing the ENTER key while in the Heading textbox will till the Editor to search the LUT for a file with a similar heading in order to help you decide whether or not you should create a new word entry if there is one already.

If you ever feel you need to rebuid your LUT you can delete all the files in the c:\\Latin\\Data\\LUT\\ directory and either extract the LUT files from this article or you can launch the LatinProject again without those files.  It will prompt you to rebuild the LUT, click yes and prepare to be patient for a good part of 24 hours.  You can stop this process by disposing of the small form that is counting down how many files remain but don't quit the program until the turning 'busy' wheel disappears.  It'll continue the rebuild the next time your launch the app.  Its not so bad if you just let run over night or whenever you're not using your computer, otherwise your computer will be sluggish while the LUT is being rebuilt.

Image 20

you'll notice the lower half of the form is gray-area.  This is space reserved for you to enter solutions that are not standard.  Have a look at the example below that shows you the famous word fero, ferre mentioned earlier.  It is an exceptional verb and needs to have its forms entered here.   When you select a word type the list of possible solutions(forms in latin textbooks) depends on the type of word it is.  A verb has conjugations for each tense and declensions for its participles.  So, being a 3rd conjugation verb at heart, fero has the same list as say the verb dico, dicere (to speak, or say).  The difference is dico, dicere is regular and needs no special attention beyond heading and definition and fero, ferre is exceptional vertually all its forms need to be entered.  To do so, you go to each conjugation listed below and select the middle radio button to the left of the name of the tense.  Top of the list below is the (alphabetically ordered) Future_Indicative_Active has its selection set to 'Regular'(left radio button) because the verb fero, ferre is regular is all forms except present indicative and imperative.  

Image 21

Scroll down to expose the Present Indicative forms and the Present Imperative that are listed further down in the alphabetical order.  Because fero, ferre  is an exceptional verb, its special forms must be entered manually.  I've already gone through most of these exceptions like this one and entered them myself, so you don't have to, but there are many newer words that you will not find in the Cassell's dictionary which you may want to include.  Doing so is not difficult using this editor.  If you look at the image below you'll see the rest of the forms listed and note that the Present_Active_Imperative, Present_Indicative_ActivePresent_Indicative_Passive. are all set to the middle radio-button which means they are User-Defined.  Moving your mouse over these radio-buttons will change the mouse cursor to reveal what these buttons mean.  They tell the Editor whether this particular form is either RegularUser-Defined or none(does not exit).  When you select User-Defined a panel with the requisite textboxes will appear beneath this list telling you where to enter the information that needs to be specified for the verb to be conjugated properly.

Image 22

as you can see, the three exceptional forms of fero, ferre are entered according to the tradition of the use of this verb.  Save the file, select view in the Options menu list and take it for a test-drive.  Let it carry a load (ferat!)

edit 2019/04/02 - there were some problems with the editor - it was adding a double of a verb tense-edit panel that resulted in one of them being visible and the second not - but more unacceptably here was that the one that was not visible was the one being recorded, making it impossible to edit that particular entry.  I don't recall where I noticed this a few weeks ago, spent twenty minutes and fixed.  it seems better now.

Dictionary List

When you're looking for a word that is not found in the LUT you may want to try looking at a list of words that are spelled similarly and see what you find.  Or you may just feel like perusing through the Cassell's dictionary, as one does.  To do so you only need to call up the Dictionary List Form by pressing F2 while in workbook mode.

Image 23

The form shown above is your Dictionary List.  You can type the word you're looking for in the upper textbox, or scroll at any time.  Its really quite straight forward, let your mouse hover over any word and a popup definition will appear.  Click on a word and you summon that word's Dictionary Definition Form.  Press ESC or click the hide button and the Dictionary List form disappears.

A Bit of Code : 

There are over 24000 word entries in the Cassell's Dictionary and including them all in a single listbox would take much memory and a long time to load.  So instead, what the form does it is has three short lists of words that it moves up and down inside the form's frame and when it is about to reach the end it is sent behind the next listbox that shows the same listed words but is only at the beginning of its list and can still show the user more in whatever direction he is scrolling.  Move the vertical scroll bar and it will take you to wherever you place it.  Its all written in the formDictionaryList.cs file (which had an unfortunate accident and won't load in the graphic editor properly ... oops, my bad).  It is broken down into three parts, the form itself(formDictionaryList), a class for the three panel that share duties sliding up and down inside the form to show you the current list of words visible(classDictionaryList_DisplayPanel) and the class that shows you one word on the screen(classDictionaryList_DisplayBox).  This last class contains the word's heading and filename so when your mouse cursor moves over it the popup text can appear quickly and if you click on it the Dictionary Definition form appears for that word.

Dictionary Content Search

The Dictionary Content Search is very convenient when you want to translate from English to Latin.  Originally, when I first typed the Latin/English half of the Cassell's dictionary into my computer I never bothered typing in the English/Latin half of that dictionary thinking I could just make a search engine that will provide me with just what I needed.  Searching the english content of the Latin word definitions is not the same as having an English/Latin dictionary but its still pretty good.  In the first publishing of this article I had not yet gotten around to writing the Content Search so it was not included in that version.  This time around, after tolerating all the issues and problems that I was finding with the LUT when I started using my spell-checker and realized so many words in the Cassell's Dictionary were not being found by the LUT ... I put in a few weeks (over a month now) working to nurse this project back to health and made giant steps towards its recovered good health and hefty upgrade.

So here we are with the Content Search.  Its a search tree like all the others except it searches the content of the dictionary files and by doing so allows you to find words by the content of their definitions.  Pretty straight forward concept.  Here's what the form looks like :

Image 24

when you either use the context menu in the Workbook mode or press F2 over selected words the Dictionary Content Search is launched and the form on the left in the image above will appear.  You can reorder the search results in either alphabetical order(seen above) or by the number of times they appear in each file (their frequency).  When you move your mouse cursor over a word a popup window appears giving you that word's definition with the searched words highlighted in bold-font.  If you want to see that word's Dictionary Definition Form, just click on the word and its Dictionary Form will appear with the search words highlighted in red.  The search engine is the same as the others, a Binary-tree with Linked-Leaf data.  So, if  you've seen these already you won't be surprised to find it looks much like the others.  And gives decent results.  Not Google ... but still.

Library & Library Search 

The Library is a collection of Latin Documents from history throughout the ages.  It includes the speeches of Cato & Catullus, among others.  Julius Caesar's Wars of Gaul, the Bible, the Donation of Constantine, Isaac Newton's Principia ("Natura nihil agit frustra, & frustra sit per plura quod fieri potest per pauciora. ") as well as the United Nations Declaration of Human Rights.  You can simply go through the Library's archives by loading a file using the File-Load menu option in the Workbook or you can type some key words you're looking for, select them and press F4 to launch a search.  Once the list of results appears you can select the entry you want and it will load into the Workbook for you to read (or edit, if you're the Administrator, erudite and brazen enough to).  Mind you Isaac was a genius, and so were the slew of other authors whose writings are still extant.  This stuff is history and probably shouldn't be edited but you're free to if you want.  All these files were downloaded from the Latin Library using a Web-Crawler I made for the express purpose.  I'd never made a web-crawler before and had to nurse it along its way but the results came out alright.  There's plenty to read or just gawk at, if you're a history buff.

edit - 2019/04/02 - I added an 'append' feature.  You can now type or copy text into the WorkBook and (with Administrator access) append the Library using the Append option in the Search menu's drop-down list.  You can also append a list of files by selecting files from a directory you specify.   When you append the library with new files from a directory in this way, those files will be moved to the appropriate  c:\\Latin\\Data\\Library\\ sub-directory .  If, after it has completed the process of entering those files into the search-engine (it can be slow) some files still remain, it is because those remaining files that were not removed were already present in the Library and were therefore not added again or overwritten.  If you delete a file from the Library - the search-engine will expect to find it and this may result in issues when it doesn't.  Its a good idea to not delete any files from this project.  If you want to rebuild any of the search engines - delete ALL the files in the Data sub-directory you want to rebuild and launch TheLatinProject again - it will prompt you to know whether or not it should rebuild those files. 

Also - (transparent to the TheLatinProjectd user) - I've made changes to the Library's content-search engine.  It is somewhat different from the other search engines because my cross-word puzzle games makes use of it to generate clues and was exceedingly (painfully) slowwwwww..... to generate clues when it tried to get a quote for a word like 'ut' or 'est' or any other word that appears in virtually every file in the library.  It was taking so long because the search-engine was giving it every file in the linked list for that given word, sometimes 1500 at a time... too much and too slow when you only need one at random and don't have time to dig through all of them while the user waits patiently for a response to his mouse-click.  so the new Library search engine is similar to a ternary tree in that it has a binary tree to find the word you're looking for and then each leaf in that tree has another binary tree that sorts the list of files that contain that word according to the frequency which that word appears in each of those files.  Then each leaf of that binary-tree(the "frequency trees" have their own record-type and file with extension .fBin) has two pointers to the Head & Tail of the linked-list of files for that frequency/word list of files.  Without the 'Tail' pointer to the end of the linked-list getting it to give me 10 'random' files for this word meant having to load all of the filenames in any given list - even if these lists are sorted by frequency they can still be quite long - so instead of using the same 10 filenames that appear at the top of the list it can now move the first filenames to the end of the list and change the Head & Tail pointers accordingly.  Cross-word puzzle will be ready soon ... i'm tired of finding issues with the LatinProject's dictionary and really want to move on to an arcade type game to distract me from all this latin... and building all these search engines takes time ... its been weeks now that I want this to end...  end it must.  perfect (probably not) but still pretty good.

Image 25

Spell Checker

The spell-checker option is available to both the User and Administrator.  It is a bit slow on the larger files, like the ones in the Library but on smaller files that you enter yourself that, when printed, are no more than a page or two, its quite adequate.  It parses the entire text into a list of words, tackles each word one word at a time (erasing all copies of the same word within its list as it goes) and searches the LUT for that word's exact spelling.  When it fails to find the word in the Look-Up Table it keeps a list of 'errors' and their locations in the text and then moves on to the next word until its processed the entire file.  After which it reloads the RichTextBox in the WorkBook panel and highlights all the words it failed to find.  

To use the spell-checker you merely press the 'SpeellChecker' menu option and its runs until it is completed.

A Bit of Code:

Implementing the spell-checker itself was not particularly difficult once the LUT was up and running until I decided it needed to be done by a BackgroundWorker and then I was a little daunted (still not all that experienced using threads) until I actually got down to it.  The LUT_Search() is what runs as a BackgroundWorker.  So, the spell-checker initially jsut asked the LUT, "is this word in the LUT" when it called the LUT_Search() function and the main thread waited there for an answer.  With a BackgroundWorker, however, this needed some re-thinking.  So the LUT_Search() backgroundworker does its job and the spellChecker has to exit and then react when the LUT_Search() is completed.  To do this it reacts to the changes made to its global Property : cSpellChecker_LUTSearchResults which is set by the LUT_Search background worker at the end of its task.  See below : 

C++
private void BckLUT_Search_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    semLUT_Search.Release();

    if (formLUT_Results.instance == null)
    {
        classLUT_LL_Record_output[] cRetVal = (classLUT_LL_Record_output[])e.Result;
        formLUT_Results.instance 
             = new formLUT_Results((strCurrentPrefix.Length > 0
                                                            ? strCurrentPrefix + "+"
                                                            : "")
                                      + strCleanWord
                                      + (strCurrentPostFix.Length > 0
                                                                  ? "+" + strCurrentPostFix
                                                                  : ""),
                                   cRetVal,
                                   ref word,
                                   ref cLibWordResourceHandler,
                                   ref cOpifex,
                                   ref classXmlLatin.instance);
        formLUT_Results.instance.Hide();
    }

    switch (eCallingFunction)
    {
        case classEnum.enuLUT_Search_CallingFunction.SpellChecker:
            classSpellchecker.instance.cSpellChecker_LUTSearchResults = (classLUT_LL_Record_output[])e.Result;
            break;
                    
        case classEnum.enuLUT_Search_CallingFunction.DictionaryEditor:
            formDictionary_Editor.instance.cHeadingSearchResults = (classLUT_LL_Record_output[])e.Result;
            break;

As you can see above, the Switch(eCallingFunction) decides what to do at the end of the LUT-Search depending on which form called it and set it to work.  In the case of the Spell-checker it sets the cSpellChecker_LUTSearchResults method and exits.  It does something similar for the Dictionary Editor whenever you press enter in the Heading textbox to tell you whether there already is a word in the LUT with that heading before you create another one.

"And that's all I have to say about that," Forrest Gump.

Tool tips

A great new addition is the Tool Tips.  These are helpful clues that will guide you along as you learn how to use this App.  You just let your mouse cursor stray immobile for a few seconds on an object on the screen and a helpful tip will soon appear.  Most of the objects on the screen have more than one tip, so it's sometimes good to go to another object and come back to the first one and see what it will say the second or third time.

A Bit of Code :  

Tool tips as you may be away if you've ever coded with them, do not innately allow for multiple tips for each object for which it is responsible.  If you haven't read my Sprite Editor 2017 article published only a month ago, then you may be interested in the classToolTip which this app uses.  It makes use of an AlphaTree (a kind of search tree) to look up the name of a given object whenver the ToolTip's PopUp event occurs.  Then it pulls information out of the AlphaTree's search result and chooses one of the Tips listed there to put on the screen by reseting that object's tip in the ToolTip before it is seen by the user.  Its really quite simple, each object is given a unique name.  Objects that appear many times such as those on the Dictionary Definition form which can have as many instances as the user will tolerate on his screen all have the same name.  Since each form must have its own ToolTip object associated to it (if it has one at all) every instance of each form tells its own tool tip to recognize the objects on its own form, then that ToolTip object is told to use the one PopUp Event common to all forms in the Application found in formLatinProject.   There is only one alphatree for all the tooltips on all the forms.  The alpha tree looks at the name of the object for which any given tooltip has had a popup event, then it tells that tooltip to set that object's Tip dynamically before it goes to screen.

Here's there classToolTip's set up function for each Tip: 

C++
public void  Tip_set(string strComponentName, string strTip)
{
    if (strComponentName.Length == 0) MessageBox.Show("Component name is blank");
    classToolTip_Element cTT_Ele = (classToolTip_Element)cAT_Objects.Search(strComponentName);

    if (cTT_Ele != null)
        cTT_Ele.Tip = strTip;
    else
    {
        cTT_Ele = new classToolTip_Element();
        cTT_Ele.strName = strComponentName;
        cTT_Ele.Tip = strTip;
        object objTT_Ele = (object)cTT_Ele;
        cAT_Objects.Insert(ref objTT_Ele, strComponentName);
    }
}

As you can see, the component's name is tied to the Tip string in the alpha tree.  If the name already has data in the tree, the new tip is added in the cTT_ELe.Tip Method's Set{}, otherwise a new element is created, given the new tip and then is run through the tree's Insert() function.  The classToolTip's Get function searches the tree and returns a new tip string via the same cTT_Ele.Tip Method which keeps track of which tip to report next.

Salve!

Latin may not be the oldest language around, but it has spawned all the Romance languages spoken today. It can be found throughout history from "Iacta alea est" to "Sic semper Tyrannis" as well as literature's great heroes like "Captain Nemo", Harry Potter and the dreaded Bellatrix. Latin is everywhere, and if you don't want to actually learn the language, or you don't have the time, energy, or will, you can still use this program to make simple translations.

And who knows when next you'll need to make a quick demonic exorcism!

 

Forture Additions 

I'm already most of the way through a Latin Crossword Puzzle generator with some fun animations.  That's coming along nicely and should be posted as a separate project soon.  There will eventually be more quizes and games but that will be for the distant future.  For now, exspecta et mox plus erit!

 

Updates

  • April 16, 2010: Updated source code, fixed a few minor bugs, moved the side panels to the bottom, and added a new 'popup' form button.
  • April 20, 2010: Moved the side panels, and added 'Vocabulary words' and flash-cards.
  • March 14, 2019 : revamp of entire project with new added features
  • April 2, 2019 : made corrections to the dictionary files, added new append-library-search feature
  • June 5, 2019 : found a bunch of Latin errors in the Cassell's Dictionary as well as the source-code while playing Latin Crossword Puzzle and made necessary changes
  • March 23, 2022 : i finally discovered this thing called 7-zip - uploaded DataFiles to Googe drive - there were some bugs that I found and fixed.  the dictionary Data files (7-zip files are newer and include these corrections ... you're slogging it with the old files, you'll need to upgrade to see these changes) have had the changes recorded in this Changes to Latin Project zipped text file.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO unemployable
Canada Canada
Christ Kennedy grew up in the suburbs of Montreal and is a bilingual Quebecois with a bachelor’s degree in computer engineering from McGill University. He is unemployable and currently living in Moncton, N.B. writing his next novel.

Comments and Discussions

 
QuestionWarnings: parameters passed as ref or out or by address may cause a runtime exception (marshal-by-reference) Pin
Christer Hartman4-Feb-16 2:01
professionalChrister Hartman4-Feb-16 2:01 
AnswerRe: Warnings: parameters passed as ref or out or by address may cause a runtime exception (marshal-by-reference) Pin
Christ Kennedy12-Nov-18 5:52
mvaChrist Kennedy12-Nov-18 5:52 
QuestionAncient Greek Pin
Christer Hartman3-Feb-16 21:40
professionalChrister Hartman3-Feb-16 21:40 
AnswerRe: Ancient Greek Pin
Christ Kennedy12-Nov-18 5:40
mvaChrist Kennedy12-Nov-18 5:40 
QuestionGreat writeup... what about Mr. Wheelock? Pin
Jksoft28-Aug-12 4:59
Jksoft28-Aug-12 4:59 
GeneralMy vote of 5 Pin
Vestitius1-Aug-12 0:50
Vestitius1-Aug-12 0:50 
QuestionWow! Pin
Bill Prada26-Jan-12 5:21
Bill Prada26-Jan-12 5:21 
GeneralDirectX Pin
jimmygyuma27-Apr-10 8:49
jimmygyuma27-Apr-10 8:49 
GeneralRe: DirectX Pin
Christ Kennedy27-Apr-10 16:16
mvaChrist Kennedy27-Apr-10 16:16 
a long time ago, in cyberspace far, far away, i recall having once downloaded something called 'DirectX'. its from Microsoft and they upgrade it every now and then though you don't always need the latest version. here's one link :
DirectX. its got many features but the one which LatinProject makes use of is strictly for the audio component. you can comment out the PlaySound() function and any reference to directX but then you'd be missing out on that option.
I plan to get rid of it and use an alternate means to play the audio. but if you do any coding at all you really should download directX.
Christ
my code is perfect until i don't find a bug...

GeneralRe: DirectX Pin
Christer Hartman3-Feb-16 21:34
professionalChrister Hartman3-Feb-16 21:34 
Generalsounds ql Pin
Seishin#22-Apr-10 1:29
Seishin#22-Apr-10 1:29 
GeneralRe: sounds ql Pin
Christ Kennedy22-Apr-10 1:48
mvaChrist Kennedy22-Apr-10 1:48 
GeneralRe: sounds ql Pin
Seishin#22-Apr-10 1:56
Seishin#22-Apr-10 1:56 
GeneralRe: sounds ql Pin
Christ Kennedy22-Apr-10 3:06
mvaChrist Kennedy22-Apr-10 3:06 
Generalmissing Source code? resolved Pin
Christ Kennedy15-Apr-10 0:36
mvaChrist Kennedy15-Apr-10 0:36 
GeneralMy vote of 1 Pin
g0got214-Apr-10 19:43
g0got214-Apr-10 19:43 
GeneralRe: My vote of 1 PinPopular
Johnny J.14-Apr-10 20:55
professionalJohnny J.14-Apr-10 20:55 
GeneralRe: My vote of 1 Pin
Christ Kennedy14-Apr-10 22:13
mvaChrist Kennedy14-Apr-10 22:13 
GeneralRe: My vote of 1 Pin
jp2code25-May-10 5:25
professionaljp2code25-May-10 5:25 
GeneralRe: My vote of 1 Pin
Christ Kennedy25-May-10 6:01
mvaChrist Kennedy25-May-10 6:01 
GeneralDear Christ! Pin
sam.hill14-Apr-10 18:30
sam.hill14-Apr-10 18:30 
GeneralRe: Dear Christ! Pin
Johnny J.14-Apr-10 20:57
professionalJohnny J.14-Apr-10 20:57 
GeneralRe: Dear Christ! [modified] Pin
Christ Kennedy14-Apr-10 22:11
mvaChrist Kennedy14-Apr-10 22:11 
GeneralRe: Dear Christ! Pin
sam.hill15-Apr-10 4:50
sam.hill15-Apr-10 4:50 
GeneralRe: Dear Christ! Pin
Christ Kennedy15-Apr-10 5:08
mvaChrist Kennedy15-Apr-10 5:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.