Search code examples
delphilocalizationdelphi-xe5

Convert ISO 3166-1 alpha-2 Country Code to localized Country Name


Is there a way to convert the two-letter Country Codes into their readable counterparts without using external ressources?

e.g. DE -> Germany, AD -> Andorra

It would be great if I could select the target language or it's using the system language, because I'd like to have them in German.


Solution

  • As @Uwe mentioned in his comment, you can use the EnumSystemGeoID and GetGeoInfo functions. The principle is that with EnumSystemGeoID function you'll enumerate geographical location identifiers and by the GetGeoInfo function query if the enumerated identifier's ISO 2-letter country / region code (info type GEO_ISO2) equals to the one of your interest. If so, then you can query for this identifier with the same function either a friendly name (info type GEO_FRIENDLYNAME), or the official name (info type GEO_OFFICIALNAME), return the result and stop the enumeration.

    Here is an example code, which might do that (unfortunately, the enumeration function does not support passing custom data, so I've used a global record variable for passing values):

    type
      TEnumData = record
        GeoCode: string;
        GeoName: string;
        Success: Boolean;
      end;
    
      GEOID = type LONG;
      GEOTYPE = type DWORD;
      GEOCLASS = type DWORD;
    
      SYSGEOTYPE = (
        GEO_NATION = $0001,
        GEO_LATITUDE = $0002,
        GEO_LONGITUDE = $0003,
        GEO_ISO2 = $0004,
        GEO_ISO3 = $0005,
        GEO_RFC1766 = $0006,
        GEO_LCID = $0007,
        GEO_FRIENDLYNAME= $0008,
        GEO_OFFICIALNAME= $0009,
        GEO_TIMEZONES = $000A,
        GEO_OFFICIALLANGUAGES = $000B,
        GEO_ISO_UN_NUMBER = $000C,
        GEO_PARENT = $000D
      );
    
      SYSGEOCLASS = (
        GEOCLASS_NATION = 16,
        GEOCLASS_REGION = 14,
        GEOCLASS_ALL = 0
      );
    
      GEO_ENUMPROC = function(GeoId: GEOID): BOOL; stdcall;
    
      function EnumSystemGeoID(GeoClass: GEOCLASS;
        ParentGeoId: GEOID; lpGeoEnumProc: GEO_ENUMPROC): BOOL; stdcall;
        external kernel32 name 'EnumSystemGeoID';
      function GetGeoInfo(Location: GEOID; GeoType: GEOTYPE;
        lpGeoData: LPTSTR; cchData: Integer; LangId: LANGID): Integer; stdcall;
        external kernel32 name {$IFDEF UNICODE}'GetGeoInfoW'{$ELSE}'GetGeoInfoA'{$ENDIF};
    
    implementation
    
    var
      // I have used this global variable due to a lack of user data parameter for the callback function
      EnumData: TEnumData;
    
    function TryGetGeoInfo(GeoId: GEOID; GeoType: GEOTYPE; out Value: string): Boolean;
    var
      Buffer: string;
      BufferLen: Integer;
    begin
      Result := False;
      BufferLen := GetGeoInfo(GeoId, GeoType, LPTSTR(Buffer), 0, 0);
      if BufferLen <> 0 then
      begin
        SetLength(Buffer, BufferLen);
        Result := GetGeoInfo(GeoId, GeoType, LPTSTR(Buffer), BufferLen, 0) <> 0;
        if Result then
          Value := Trim(Buffer);
      end;
    end;
    
    function EnumGeoInfoProc(GeoId: GEOID): BOOL; stdcall;
    var
      S: string;
    begin
      Result := TryGetGeoInfo(GeoId, GEOTYPE(GEO_ISO2), S);
      if Result and (S = EnumData.GeoCode) then
      begin
        // stop the enumeration since we've found the country by its ISO code
        Result := False;
        // return the success flag and try to return the friendly name of the country to the
        // EnumData.GeoName record field; you can optionally query the GEO_OFFICIALNAME
        EnumData.Success := TryGetGeoInfo(GeoId, GEOTYPE(GEO_FRIENDLYNAME), EnumData.GeoName);
      end;
    end;
    
    function TryGetCountryNameByISO2(const Code: string; out Name: string): Boolean;
    begin
      // here is the brainless part using global record variable (because the function used
      // here with its callback does not support passing user data); no, you cannot tune it
      // up by making the callback function nested
      EnumData.GeoCode := Code;
      EnumData.Success := False;
    
      if not EnumSystemGeoID(GEOCLASS(GEOCLASS_NATION), 0, EnumGeoInfoProc) then
        RaiseLastOSError;
    
      Result := EnumData.Success;
      if Result then
        Name := EnumData.GeoName;
    end;
    

    And a possible usage:

    var
      S: string;
    begin
      if TryGetCountryNameByISO2('DE', S) then
        ShowMessage(S);
    end;