Setting a default language with dxgettext

 Delphi, dzLib  Comments Off on Setting a default language with dxgettext
Nov 102014
 

By default, if no translation for a language is available, dxgettext will not do any translation but use the strings as they are in the source code. Sometimes this is not desirable. e.g.

  • Your customer does not understand the source language (e.g. your source language is not English but say German)
  • You are using dxgettext to convert special characters from a placeholder (e.g. “(R)” or “[deg]”) to the actual character (“®” or “°”)

In these cases you’d probably want the translation to default to a language that is actually supplied.

dxgettext doesn’t seem to have this feature (I looked quite hard) so I implemented it myself.

unit u_dzTranslator;

interface

// ... other stuff ...

///<summary>
/// Sets the language to use </summary>
procedure UseLanguage(_LanguageCode: string);

///<summary>
/// gets a list of languages for which translations are available </summary>
procedure GetListOfLanguages(const _Domain: string; _Codes: TStrings;
  _Languages: TStrings = nil);

///<summary>
/// Sets the language to use if the desired language is not available,
/// defaults to English </summary>
procedure SetDefaultLanguage(const _LanguageCode: string);

// ... other stuff ...

implementation

uses
  gnugettext;

// ... other stuff ...

const
  DEFAULT_LANGUAGE = 'en';
var
  gblDefaultLanguage: string = DEFAULT_LANGUAGE;

procedure UseLanguage(_LanguageCode: string);
var
  Codes: TStringList;
  CurLang: string;
  i: Integer;
  p: Integer;
begin
  gnugettext.UseLanguage(_LanguageCode);

  CurLang := gnugettext.GetCurrentLanguage;
  Codes := TStringList.Create;
  try
    GetListOfLanguages('default', Codes);
    for i := 0 to Codes.Count - 1 do begin
      if SameText(CurLang, Codes[i]) then begin
        // There is a translation for this language and country, everything is fine
        Exit; //-->
      end;
    end;
    // no translation found, try without the country code
    p := Pos('_', CurLang);
    if p <> 0 then begin
      CurLang := Copy(CurLang, 1, p - 1);
      for i := 0 to Codes.Count - 1 do begin
        if SameText(CurLang, Codes[i]) then begin
          // There is a translation for this language but not country, we can live with that
          Exit; //-->
        end;
      end;
    end;
  finally
    FreeAndNil(Codes);
  end;

  // we found no translation for this language, so we use the default language
  gnugettext.UseLanguage(gblDefaultLanguage);
end;

procedure SetDefaultLanguage(const _LanguageCode: string);
begin
  if _LanguageCode = '' then
    gblDefaultLanguage := DEFAULT_LANGUAGE
  else
    gblDefaultLanguage := _LanguageCode;
  UseLanguage(gnugettext.GetCurrentLanguage);
end;

procedure GetListOfLanguages(const _Domain: string; _Codes: TStrings; _Languages: TStrings = nil);
var
  i: Integer;
begin
  _Codes.Clear;
  gnugettext.DefaultInstance.GetListOfLanguages(_Domain, _Codes);
  if Assigned(_Languages) then begin
    _Languages.Clear;
    for i := 0 to _Codes.Count - 1 do begin
      _Languages.Add(languagecodes.getlanguagename(_Codes[i]));
    end;
  end;
end;

// ... other stuff ...

initialization
  SetDefaultLanguage(DEFAULT_LANGUAGE);
end.

Apart from the obvious, that is, setting a unit global variable to the desired default language, which itself defaults to English, this code changes the way UseLanguageWorks. It now does:

  • Call gnugettext.UseLanguage to let gnugettext do its stuff
  • Call gnugettext.GetCurrentLanguage to get the language that gnugettext uses (just in case gnugettext changes it from what was set with UseLanguage).
  • Gets a list of all supported translations
  • Tries to find a matching translation for the desired language and country.
  • If not found, tries to find a matching translation for the desired language, ignoring the country
  • If not found, changes the language to the default language.

Note that I just wrote this code, it might still contain bugs and is probably far from perfect. I will put it into the unit u_dzTranslator of my dzlib library and will fix any bugs I find in the future there.

 Posted by on 2014-11-10 at 13:16

When CloseHandle does not close the handle

 Delphi  Comments Off on When CloseHandle does not close the handle
Nov 052014
 

Today I spent several hours hunting down a problem with opening a COM port. Basically the program opens a COM port, writes some bytes to it, reads an answer and closes it again. This determines whether the expected device is connected to the COM port or not.

If the answer is the expected one, so the device is connected and active, the detection loop exists (the COM port has been closed using CloseHandle).

Now the actual communication with the device starts. The first thing is does, is open the same COM port again. Which fails with the error code 5 (ERROR_ACCESS_DENIED). WTF?

Of course, I tried to debug the issue using the Delphi integrated debugger. While I stepped through the code, the error disappeared, only to come back when I just run the code without stepping through it. This was reproducible (I love reproducible errors.).

When something like this happens, you can be sure it is a timing issue. Either your program is multithreaded and the issue is one thread doing something while the other is doing something else that conflicts with the first thread (called race condition or deadlock, depending on the outcome). Or it’s not your own threads but somebody else’s.

In my case this StackOverflow question seems to be about the same issue and the accepted answer was mentioning the FTDI drivers I am using (the COM port is actually a USB serial adapter). These drivers apparently do not immediately close the port when the handle is closed so opening it again can fail. It turned out that a retry loop when opening the COM port solved the issue. Here is the code I ended up using:

procedure TCustomComPort.CreateHandle;
const
  MAX_TRIES = 10;
var
  Tries: integer;
  OK: boolean;
begin
  Tries := 0;
  repeat
    Inc(Tries);
    FHandle := CreateFile(
      PChar('\\.\' + FPort),
      GENERIC_READ or GENERIC_WRITE,
      0,
      nil,
      OPEN_EXISTING,
      FILE_FLAG_OVERLAPPED,
      0);
    OK := (FHandle <> INVALID_HANDLE_VALUE);
    if not OK then
      Sleep(10 * Tries); // it wasn't enough to Sleep(10) here
  until OK or (Tries >= MAX_TRIES);

  if not OK then
    CallException(CError_OpenFailed, GetLastError);
end;

Some of you might recognize part of this code from the ComPort library for Delphi and C++. (But probably not, because the original code is just three lines. 😉 )

Of course the real debugging was more complex than I described above because the program is multi threaded and the detection is done in one thread while the actual communication is done with the foreground thread writing to the port and the answers being processed by a background thread. So of course, at first I suspected an error in my code, actually found one, fixed it, just to run into the next problem. Finally it turned out to be the driver problem described here.

 Posted by on 2014-11-05 at 19:48

class as interface: Operator not applicable to this operand

 Delphi  Comments Off on class as interface: Operator not applicable to this operand
Nov 042014
 

Once in a while I run into this problem and every single time it takes me forever to remember the cause:

Say, you have got an interface and a class implementing that interface:

type
  IMyInterface = interface
    function asMyInterface: IMyInterface;
  end;

type
  TMyClass = class(TInterfacedObject, IMyInterface)
  private
    function asMyInterface: IMyInterface;
  end;

[...]

function TMyClass.asMyInterface: IMyInterface;
begin
  Result := Self as IMyInterface; // compile error here
end;

This will fail to compile in the marked line with the error “Operator not applicable to this operand type”.

The reason is simple: In order for the as operator to work on interfaces, the interface must have a GUID assigned to it. You do that in the Delphi IDE by positioning the cursor after the interface keyword and press Shift+Ctrl+G. As soon as your interface declaration looks like this …

type
  IMyInterface = interface ['{93903D10-58F7-41B0-AFB1-2A8E17F828EF}']
    function asMyInterface: IMyInterface;
  end;

… the code will compile.

(Don’t just copy this code, you will need to generate your own unique GUID as described above! Otherwise you will experience strange things.)

 Posted by on 2014-11-04 at 17:11