Translating file filters

You know these innocuous file filters used by Open / Save dialogs? They usually look like this:

First Filetype (*.ex1)|*.ex1|Second Filetype (*.ex2)|*.ex2|All Files (*.*)|*.*

And since typing them is so difficult because you must remember to type all these pipe symbols, Delphi provides a property editor for it.

Is there anybody who actually likes this property editor? I think it’s even more annoying than directly typing these strings. But why, oh why do I have to type the file filter twice? Once for the description and once for the actual filter?

It gets worse: Just imagine you localize an application that uses many such file filters. So you have lots of strings that contain the ‘All Files (*.*)|*.*’ part, but are different because they support different, but partly overlapping file types. You end up translating the same text over and over again:

First Filetype (*.ex1)|*.ex1|All Files (*.*)|*.*
Second Filetype (*.ex2)|*.ex2|All Files (*.*)|*.*
First Filetype (*.ex1)|*.ex1|Second Filetype (*.ex2)|*.ex2|All Files (*.*)|*.*

Maybe you even spelled ‘All Files’ inconsistently as ‘all files’, ‘all Files’ or ‘All files’. Which confuses the translator and looks bad in your program.

And now add human error: The translator overlooks the part with the actual filter because he just doesn’t know that the string contains two different kinds of text: The first part is human readable and must be translated, the second part is used by the computer to filter files, and it must not be translated, it must not even be changed. Your translator might create translations like this:

Erster Dateityp (*.ex1)|All Files|*.*
Zweiter Dateityp *.ex2|All Files (*.*)|*.*
Erster Dateityp (*.ex1)|*.ex1|Zweiter Dateityp  (*.ex2)|*.*e|All Files (*.*)|*.*

Did you notice the errors? Nobody really does and the program gets shipped. Now you have not only a translation error but also a bug in your program, because these strings are no longer valid file filters.

After this happened to me the gazillionth time, I had enough, so I created the following helper code:

type
  IdzFileFilterBuilder = interface ['{1EEE52D6-EA31-4C4D-8454-32B2C2BE1814}']
    function Add(const _Description: string; const _Mask: string; _AddMaskToDesc: Boolean = True): IdzFileFilterBuilder;
    function AddAvi: IdzFileFilterBuilder;
    function AddBmp: IdzFileFilterBuilder;
    // ... some more ...
    function Filter: string;
  end;

function FileFilterBuilder(_IncludeAllFiles: Boolean = True; const _AllSupported: string = ''): IdzFileFilterBuilder;

The idea is to call the FileFilterBuilder function once and add any filters you need to the returned interface by chaining the calls like this:

  od.Filter := FileFilterBuilder.AddBmp.AddJpg.AddGif.Value;

Or, if you like this formatting better:

  od.Filter := FileFilterBuilder
    .AddBmp
    .AddJpg
    .AddGif
    .Value;

No unwieldy strings that mix human readable and computer interpreted text, just a simple list of easily readable file extensions. And even if you want to do something more unusual like having one entry for all supported file types and adding a file type that is not predefined by the interface like this:

  od.Filter := FileFilterBuilder(true, 'All supported types')
    .AddBmp
    .AddJpg
    .AddGif
    .Add('Some other graphics format', '*.blub')
    .Value;

It is still quite readable. Of course the above strings must be translated, so we add _() function calls for dxgettext:

  od.Filter := FileFilterBuilder(true, _('All supported types'))
    .AddBmp
    .AddJpg
    .AddGif
    .Add(_('Some other graphics format'), '*.blub')
    .Value;

Notice that only the part that actually needs translation is inside the _() translation marker? There is no more way for the translator to change essential strings of your programs. He might create wrong translations but your program will still run.

Of course that was only the interface. I guess, implementing it won’t be too hard for most of my readers. But since I am in a generous mood, I give it to you for free (no, that’s not an April fools joke). Just download it from my dzlib utility library on sourceforge. The file is u_dzDialogUtils and it contains a few more of the predefined .AddXxx methods. Also, the library will soon contain the German translations for the filter strings, if you are interested. You will also need the pseudo templates in the templates subdirectory.

Oh, did I mention this little gem:

function TdzFileFilterBuilder.AddPicture: IdzFileFilterBuilder;
begin
  Result := Add(_('Picture files'), '*.bmp;*.jpg;*.jpeg').AddBmp.AddJpg;
end;

It adds three entries:

  • Picture Files (*.bmp;*.jpg;*.jpeg)
  • Bitmap Files (*.bmp)
  • JPEG files (*.jpg;*.jpeg)

Resulting in the following file filter string:

'Picture Files (*.bmp;*.jpg;*.jpeg)|*.bmp;*.jpg;*.jpeg|Bitmap Files (*.bmp)|*.bmp|JPEG files (*.jpg;*.jpeg)|*.jpg;*.jpeg'

(with the ‘All files’ entry added, if you passed true to the FileFilterBuilder function.)