How to detect system language in delphi for multi-language project? - delphi

I need to translate a program in others languages, actually I have the same program in 3 languages (english, spanish, portuguese), but I translated, recompiled, and I have 3 separate executables. And add more languages, and keep links, and adding new functions is driving me crazy.
So now I decided to keep a single executable, and a external language file, so adding new languages does not need recompiling, just editing the language file with a text editor, and everything is ok.
I want to keep all languages in a single external file. like international.lang
[portuguese]
greeting="Bem-vindo"
[spanish]
greeting="Ben venido"
if the file international.lang is not there, or your language is not on the file, the program will launch in english by default, with no errors. Just like most multi-languages programas based on resources.
So the question is, how detect the Windows language in delphi?
Any thoughts on my approach?
There is any way to replace all captions on dialogs programaticly?
ps: I'm using delphi7, and I can't find any component that is free that is good.

You can use the GetSystemDefaultLCID function to get the locale identifier and then use the VerLanguageName function to resolve the language associated name. or use the GetLocaleInfo function
Check this sample
{$APPTYPE CONSOLE}
uses
Windows,
SysUtils;
procedure Test_VerLanguageName;
var
wLang : LangID;
szLang: Array [0..254] of Char;
begin
wLang := GetSystemDefaultLCID;
VerLanguageName(wLang, szLang, SizeOf(szLang));
Writeln(szLang);
end;
procedure Test_GetLocaleInfo;
var
Buffer : PChar;
Size : integer;
begin
Size := GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALE_SENGLANGUAGE, nil, 0);
GetMem(Buffer, Size);
try
GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALE_SENGLANGUAGE, Buffer, Size);
Writeln(Buffer);
finally
FreeMem(Buffer);
end;
end;
begin
try
Test_VerLanguageName;
Test_GetLocaleInfo;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
Note : Starting with Windows Vista exists new functions to get the same locale information, check these functions GetLocaleInfoEx, GetUserDefaultLocaleName and GetSystemDefaultLocaleName

I have the same problem although I have to deal with only two languages: English (default) and Polish.
I tried all the solutions listed above and none of them was working. I was changing system setting, rebooting etc. and always receiving language English.
When switched to Polish everything was displayed in Polish, all Polish locales were set but my application was receiving English as the OS language. After many tries I came across with quite easy and reliable workaround (I do not call it solution) that is good if you have to deal with a small number of languages.
So the trick is to check in what language the language list is returned by TLanguages.
function GetLang: Integer; //lcid
const
lcidEnglish = $9;
lcidPolish = $415;
var Idx: Integer;
begin
Result := Languages.IndexOf(lcidPolish);
if (Result > 0) and
(Languages.Name[Result].StartsWith('Polski', True)) //'Polski'is the Polish name of the language
then Result := lcidPolish
else Result := lcidEnglish;
end;
You can do the same for your three languages.
Hope it helps.

Related

Exception with German Umlaut characters in TMemIniFile.Create

I have an .URL file which contains the following text which contains a German Umlaut character:
[InternetShortcut]
URL=http://edn.embarcadero.com/article/44358
[MyApp]
Notes=Special Test geändert
Icon=default
Title=Bug fix list for RAD Studio XE8
I try to load the text with TMemIniFile:
uses System.IniFiles;
//
procedure TForm1.Button1Click(Sender: TObject);
var
BookmarkIni: TMemIniFile;
begin
// The error occurs here:
BookmarkIni := TMemIniFile.Create('F:\Bug fix list for RAD Studio XE8.url',
TEncoding.UTF8);
try
// Some code here
finally
BookmarkIni.Free;
end;
end;
This is the error message text from the debugger:
Project MyApp.exe raised exception class EEncodingError with message
'No mapping for the Unicode character exists in the target multi-byte
code page'.
When I remove the word with the German Umlaut character "geändert" from the .URL file then there is NO error.
But that's why I use TMemIniFile, because TIniFile does not work here when the text in the .URL file contains Unicode characters. (There could also be other Unicode characters in the .URL file).
So why I get an exception here in TMemIniFile.Create?
EDIT: Found the culprit: The .URL file is in ANSI format. The error does not happen when the .URL file is in UTF-8 format. But what can I do when the file is in ANSI format?
EDIT2: I've created a workaround which does work BOTH with ANSI and UTF-8 files:
procedure TForm1.Button1Click(Sender: TObject);
var
BookmarkIni: TMemIniFile;
BookmarkIni_: TIniFile;
ThisFileIsAnsi: Boolean;
begin
try
ThisFileIsAnsi := False;
BookmarkIni := TMemIniFile.Create('F:\Bug fix list for RAD Studio XE8.url',
TEncoding.UTF8);
except
BookmarkIni_ := TIniFile.Create('F:\Bug fix list for RAD Studio XE8.url');
ThisFileIsAnsi := True;
end;
try
// Some code here
finally
if ThisFileIsAnsi then
BookmarkIni_.Free
else
BookmarkIni.Free;
end;
end;
What do you think?
It is not possible, in general, to auto-detect the encoding of a file from its contents.
A clear demonstration of this is given by this article from Raymond Chen: The Notepad file encoding problem, redux. Raymond uses the example of a file containing these two bytes:
D0 AE
Raymond goes on to show that this is a well formed file with the following four encodings: ANSI 1252, UTF-8, UTF-16BE and UTF-16LE.
The take home lesson here is that you have to know the encoding of your file. Either agree it by convention with whoever writes the file. Or enforce the presence of a BOM.
You need to decide on what the encoding of the file is, once and for all. There's no fool proof way to auto-detect this, so you'll have to enforce it from your code that creates these files.
If the creation of this file is outside your control, then you are more or less out of luck. You can try to rely of the BOM (Byte-Order-Mark) at the beginning of the file (which should be there if it is a UTF-8 file). I can't see from the specification of the TMemIniFile what the CREATE constructor without an encoding parameter assumes about the encoding of the file (my guess is that it follows the BOM and if there's no such thing, it assumes ANSI, ie. system codepage).
One thing you can do - if you decide to stick to your current method - is to change your code to:
procedure TForm1.Button1Click(Sender: TObject);
var
BookmarkIni: TCustomIniFile;
begin
// The error occurs here:
try
BookmarkIni := TMemIniFile.Create('F:\Bug fix list for RAD Studio XE8.url',
TEncoding.UTF8);
except
BookmarkIni := TIniFile.Create('F:\Bug fix list for RAD Studio XE8.url');
end;
try
// Some code here
finally
BookmarkIni.Free;
end;
end;
You don't need two separate variables, as both TIniFile and TMemIniFile (as well as TRegistryIniFile) all have a common ancestor: TCustomIniFile. By declaring your variable as this common ancestor, you can instantiate (create) it as any of the class types that inherit from TCustomIniFile. The actual (run-time) type is determined depending on which construtcor you're calling to create.
But first, you should try to use
BookmarkIni := TMemIniFile.Create('F:\Bug fix list for RAD Studio XE8.url');
ie. without any encoding specified, and see if it works with both ANSI and UTF-8 files.
EDIT: Here's a test program to verify my claim made in the comments:
program Project21;
{$APPTYPE CONSOLE}
uses
IniFiles, System.SysUtils;
const
FileName = 'F:\Bug fix list for RAD Studio XE8.url';
var
TXT : TextFile;
procedure Test;
var
BookmarkIni: TCustomIniFile;
begin
try
BookmarkIni := TMemIniFile.Create(FileName,TEncoding.UTF8);
except
BookmarkIni := TIniFile.Create(FileName);
end;
try
Writeln(BookmarkIni.ReadString('MyApp','Notes','xxx'))
finally
BookmarkIni.Free;
end;
end;
begin
try
AssignFile(TXT,FileName); REWRITE(TXT);
try
WRITELN(TXT,'[InternetShortcut]');
WRITELN(TXT,'URL=http://edn.embarcadero.com/article/44358');
WRITELN(TXT,'[MyApp]');
WRITELN(TXT,'Notes=The German a umlaut consists of the following two ANSI characters: '#$C3#$A4);
WRITELN(TXT,'Icon=default');
WRITELN(TXT,'Title=Bug fix list for RAD Studio XE8');
finally
CloseFile(TXT)
end;
Test;
ReadLn
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
The rule of thumb - to read data (file, stream whatever) correctly you must know the encoding! And the best solution is to let user to choose encoding or force one e.g. utf-8.
Moreover, the information ANSI does make things easier without code page.
A must read - The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
Other approach is to try to detect encoding (like browsers do with sites if no encoding specified). Detecting UTF is relatively easy if BOM exists, but more often is omitted. Take a look Mozilla's universalchardet or chsdet.

How can I call ioctl for interface list, or other ioctl stuff, on Free Pascal?

I've been Googling up and down, searching on the Free Pascal Wiki and even on some (obscure) mailing lists and have come completely empty on how to use ioctl() or fpioctl() on Free Pascal.
I have this bug report from Free Pascal's Bugtrack with code that enumerates the network interfaces.
The code does not compile since the libc unit has been deprecated.
A lot of similar questions about libc point to this wiki entry that talks about it's demise.
It does not give you any indication on where the SIOC*IF* stuff has gone.
Does that mean that most of ioctl functionality has gone?
Using find and grep, under /usr/share/fpcsrc/<fpc-version>/, I've been able to track some usage of fpioctl() in relation to terminals with the termios unit. Other stuff uses it but it looks like it's under other OSs.
Apart from that I'm unable to find anything of any use if you want to do something like:
if ioctl(sock, SIOCGIFCONF, #ifc)= 0 then begin
{...}
end;
So, can anyone from the Free Pascal Community give me a pointer to what's the current situation if one wants to do ioctl calls under Linux?
Does BaseUnix.FpIOCtl meet your use case? Have a look at the BaseUnix documentation. I found an example of using it here (reposted below).
program testrpi;
{$mode objfpc}{$H+}
uses
baseUnix,
classes,
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
sysutils;
const
I2C_SLAVE = 1795;
var
buf : packed array [0..1] of char;
c : char;
devPath : string = '/dev/i2c-1';
handle : Cint;
iDevAddr : Cint = $04;
begin
try
handle := fpopen(devPath,O_RDWR);
fpIOCtl(handle, I2C_SLAVE, pointer(iDevAddr));
except
writeln('Error initalizing i2c');
halt;
end;
while true do begin
write('Enter digit 1-9:');
readln(c);
if (not(c in ['1'..'9'])) then begin
writeln('oops - try again');
continue;
end;
buf[0] := chr(ord(c) - ord('0'));
try
fpwrite(handle, buf, 1);
except
writeln('Error writing');
halt;
end; //try
buf[0] := #99;
sleep(10);
try
fpread(handle, buf, 1);
except
writeln('Error reading');
halt;
end; //try
writeln('buf=', ord(buf[0]));
end; //while
fpclose(handle);
end.
The fpioctl bit has been answer by Mick and the FAQs. As for the constants, as the libc unit faq explains there is no clear cut solution, and thus for the more specialized constants there are no replacements.
OS specific constants should go in OS specific units (linux), and (somewhat) portable ones are usually grouped with the calls of the functionality they are for.
The old libc header was an rough header translation that was cleaned up somewhat, which was manageable for 32-bit Linux only, but unusable for a nix abstraction or even "just" multiplatform Linux. It was therefore abandoned.
In short it is best to either make a simple unit that abstracts the relevant parts or to just define the constants locally.

How can I translate the Firemonkey's Resource Strings?

I'm иrazilian and I need the Firemonkey's Resource Strings in my language, for example when I use the dialogs. I couldn't find a way to translate it. Does someone know how to do it?
What you need is something like this, but for FMX. In a quick search, the only file that I've found in help for Delphi XE2 is FMX.Consts. You take that file, translate it, and then put the translated file in your project.
Take care when Delphi got an update. The original file can be changed and you will need to update your translation. Also, you will probably want to change any others files that have Resource Strings, and are in use by your project.
Finally, I'm not expert in this, but if you are planing use multi-language, this could be not the better approach.
Translating FMX forms is done with the TLang component, although that won't work for most dialogs.
You can change dialogs using FMX.Consts.pas, but it fixes the language during compilation.
If you want to check the language version of the host OS in runtime, you should correct FMX.Platform.Android.pas or FMX.Platform.iOS.pas.
For Android, in FMX.Platform.Android.pas, in procedure TPlatformAndroid.MessageDialog... find ButtonCaptions and surround it with your own function, for example: ZSTranslate( ButtonCaptions[B]).
Declare ZSTranslate in the following way:
function ZSTranslate(txt: String):String;
var
LocaleSvc: IFMXLocaleService;
begin
LocaleSvc := TPlatformServices.Current.GetPlatformService(IFMXLocaleService) as IFMXLocaleService;
result:=txt;
if LocaleSvc.GetCurrentLangID ='your_language_two_letter_id' then
begin
if txt= 'Yes' then
result := 'yes in your language'
else
if txt= 'No' then
result := 'no in your language'
else
if txt= 'Confirm' then
result := 'confirm in your language'
else
if txt= 'Cancel' then
result := 'cancel in your language';
end
end;
Place ZSTranslate somewhere above TPlatformAndroid.MessageDialog in a copy of FMX.Platform.Android.pas, and add this corrected version of FMX.Platform.Android.pas to your project.
Note that above example is very simple, and as far as I remember there is unsolved case in Embarcadero quality system, suggesting translate method here (so TLang should work ok). I did not try with translate, my version did the job (as there are just a few button labels in dialogs and I wanted only two different languages).
For iOS you should look in FMX.Platform.iOS.pas for function TPlatformCocoaTouch.MessageDialog. Please note, that there are two overloaded versions. There are also MsgTitles and ButtonCaptions in iOS, as dialogs at iOS show captions.
PS. For Polish I had to correct also GetCurrentLangID method, because it always returned 'en' - please double-check the result for your language.
The versions that worked for me:
in FMX.Platform.iOS.pas:
function TPlatformCocoaTouch.GetCurrentLangID: string;
var
lngs : NSArray;
CurrentLocale: NSLocale;
LanguageISO: NSString;
begin
lngs := TNSLocale.OCClass.preferredLanguages;
LanguageISO:= TNSString.Wrap(lngs.objectAtIndex(0));
//CurrentLocale := TNSLocale.Wrap(TNSLocale.OCClass.currentLocale);
//LanguageISO := TNSString.Wrap(CurrentLocale.objectForKey((NSLocaleLanguageCode as ILocalObject).GetObjectID));
Result := UTF8ToString(LanguageISO.UTF8String);
if Length(Result) > 2 then
Delete(Result, 3, MaxInt);
end;
in FMX.Platform.Android.pas:
function TPlatformAndroid.GetCurrentLangID: string;
var
Locale: JLocale;
begin
Locale := TJLocale.JavaClass.getDefault;
Result := JStringToString(Locale.getLanguage);//getISO3Language); //zs
if Length(Result) > 2 then
Delete(Result, 3, MaxInt);
end;

How to save string into text files in Delphi?

What is the easiest way to create and save string into .txt files?
Use TStringList.
uses
Classes, Dialogs; // Classes for TStrings, Dialogs for ShowMessage
var
Lines: TStrings;
Line: string;
FileName: string;
begin
FileName := 'test.txt';
Lines := TStringList.Create;
try
Lines.Add('First line');
Lines.Add('Second line');
Lines.SaveToFile(FileName);
Lines.LoadFromFile(FileName);
for Line in Lines do
ShowMessage(Line);
finally
Lines.Free;
end;
end;
Also SaveToFile and LoadFromFile can take an additional Encoding in Delphi 2009 and newer to set the text encoding (Ansi, UTF-8, UTF-16, UTF-16 big endian).
Actually, I prefer this:
var
Txt: TextFile;
SomeFloatNumber: Double;
SomeStringVariable: string;
Buffer: Array[1..4096] of byte;
begin
SomeStringVariable := 'Text';
AssignFile(Txt, 'Some.txt');
Rewrite(Txt);
SetTextBuf(Txt, Buffer, SizeOf(Buffer));
try
WriteLn(Txt, 'Hello, World.');
WriteLn(Txt, SomeStringVariable);
SomeFloatNumber := 3.1415;
WriteLn(Txt, SomeFloatNumber:0:2); // Will save 3.14
finally CloseFile(Txt);
end;
end;
I consider this the easiest way, since you don't need the classes or any other unit for this code. And it works for all Delphi versions including -if I'm not mistaken- all .NET versions of Delphi...
I've added a call to SetTextBuf() to this example, which is a good trick to speed up textfiles in Delphi considerably. Normally, textfiles have a buffer of only 128 bytes. I tend to increase this buffer to a multiple of 4096 bytes. In several cases, I'va also implemented my own TextFile types, allowing me to use these "console" functions to write text to memo fields or even to another, external application! At this location is some example code (ZIP) I wrote in 2000 and just modified to make sure it compiles with Delphi 2007. Not sure about newer Delphi versions, though. Then again, this code is 10 years old already.These console functions have been a standard of the Pascal language since it's beginning so I don't expect them to disappear anytime soon. The TtextRec type might be modified in the future, though, so I can't predict if this code will work in the future... Some explanations:
WA_TextCustomEdit.AssignCustomEdit allows text to be written to CustomEdit-based objects like TMemo.
WA_TextDevice.TWATextDevice is a class that can be dropped on a form, which contains events where you can do something with the data written.
WA_TextLog.AssignLog is used by me to add timestamps to every line of text.
WA_TextNull.AssignNull is basically a dummy text device. It just discards anything you write to it.
WA_TextStream.AssignStream writes text to any TStream object, including memory streams, file streams, TCP/IP streams and whatever else you have.
Code in link is hereby licensed as CC-BY
Oh, the server with the ZIP file isn't very powerful, so it tends to be down a few times every day. Sorry about that.
The IOUtils unit which was introduced in Delphi 2010 provides some very convenient functions for writing/reading text files:
//add the text 'Some text' to the file 'C:\users\documents\test.txt':
TFile.AppendAllText('C:\users\documents\text.txt', 'Some text', TEncoding.ASCII);
Or if you are using an older version of Delphi (which does not have the for line in lines method of iterating a string list):
var i : integer;
begin
...
try
Lines.Add('First line');
Lines.Add('Second line');
Lines.SaveToFile(FileName);
Lines.LoadFromFile(FileName);
for i := 0 to Lines.Count -1 do
ShowMessage(Lines[i]);
finally
Lines.Free;
end;
If you're using a Delphi version >= 2009, give a look to the TStreamWriter class.
It will also take care of text file encodings and newline characters.
procedure String2File;
var s:ansiString;
begin
s:='My String';
with TFileStream.create('c:\myfile.txt',fmCreate) do
try
writeBuffer(s[1],length(s));
finally
free;
end;
end;
Care needed when using unicode strings....

Getting size of a file in Delphi 2010 or later?

Delphi 2010 has a nice set of new file access functions in IOUtils.pas (I especially like the UTC versions of the date-related functions). What I miss so far is something like
TFile.GetSize (const Path : String)
What is the Delphi 2010-way to get the size of a file? Do I have to go back and use FindFirst to access TSearchRec.FindData?
Thanks.
I'm not sure if there's a "Delphi 2010" way, but there is a Windows way that doesn't involve FindFirst and all that jazz.
I threw together this Delphi conversion of that routine (and in the process modified it to handle > 4GB size files, should you need that).
uses
WinApi.Windows;
function FileSize(const aFilename: String): Int64;
var
info: TWin32FileAttributeData;
begin
result := -1;
if NOT GetFileAttributesEx(PChar(aFileName), GetFileExInfoStandard, #info) then
EXIT;
result := Int64(info.nFileSizeLow) or Int64(info.nFileSizeHigh shl 32);
end;
You could actually just use GetFileSize() but this requires a file HANDLE, not just a file name, and similar to the GetCompressedFileSize() suggestion, this requires two variables to call. Both GetFileSize() and GetCompressedFileSize() overload their return value, so testing for success and ensuring a valid result is just that little bit more awkward.
GetFileSizeEx() avoids the nitty gritty of handling > 4GB file sizes and detecting valid results, but also requires a file HANDLE, rather than a name, and (as of Delphi 2009 at least, I haven't checked 2010) isn't declared for you in the VCL anywhere, you would have to provide your own import declaration.
Using an Indy unit:
uses IdGlobalProtocols;
function FileSizeByName(const AFilename: TIdFileName): Int64;
You can also use DSiFileSize from DSiWin32. Works in "all" Delphis. Internally it calls CreateFile and GetFileSize.
function DSiFileSize(const fileName: string): int64;
var
fHandle: DWORD;
begin
fHandle := CreateFile(PChar(fileName), 0, 0, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if fHandle = INVALID_HANDLE_VALUE then
Result := -1
else try
Int64Rec(Result).Lo := GetFileSize(fHandle, #Int64Rec(Result).Hi);
finally CloseHandle(fHandle); end;
end; { DSiFileSize }
I'd like to mention few Pure Delphi ways. Though i think Deltics made a most speed-effective answer for Windows platform, yet sometimes you want just rely on RTL and also make portable code that would work in Delphi for MacOS or in FreePascal/Virtual Pascal/whatever.
There is FileSize function left from Turbo Pascal days.
http://turbopascal.org/system-functions-filepos-and-filesize
http://docwiki.embarcadero.com/CodeExamples/XE2/en/SystemFileSize_(Delphi)
http://docwiki.embarcadero.com/Libraries/XE2/en/System.FileSize
The sample above lacks "read-only" mode setting. You would require that to open r/o file such as one on CD-ROM media or in folder with ACLs set to r/o. Before calling ReSet there should be zero assigned to FileMode global var.
http://docwiki.embarcadero.com/Libraries/XE2/en/System.FileMode
It would not work on files above 2GB size (maybe with negative to cardinal cast - up to 4GB) but is "out of the box" one.
There is one more approach, that you may be familiar if you ever did ASM programming for MS-DOS. You Seek file pointer to 1st byte, then to last byte, and check the difference.
I can't say exactly which Delphi version introduced those, but i think it was already in some ancient version like D5 or D7, though that is just common sense and i cannot check it.
That would take you an extra THandle variable and try-finally block to always close the handle after size was obtained.
Sample of getting length and such
http://docwiki.embarcadero.com/Libraries/XE2/en/System.SysUtils.FileOpen
http://docwiki.embarcadero.com/Libraries/XE2/en/System.SysUtils.FileSeek
Aside from 1st approach this is int64-capable.
It is also compatible with FreePascal, though with some limitations
http://www.freepascal.org/docs-html/rtl/sysutils/fileopen.html
You can also create and use TFileStream-typed object - which was the primary, officially blessed avenue for file operations since Delphi 1.0
http://www.freepascal.org/docs-html/rtl/classes/tfilestream.create.html
http://www.freepascal.org/docs-html/rtl/classes/tstream.size.html
http://docwiki.embarcadero.com/Libraries/XE2/en/System.Classes.TFileStream.Create
http://docwiki.embarcadero.com/Libraries/XE2/en/System.Classes.TStream.Size
As a side note, this avenue is of course integrated with aforementioned IOUtils unit.
http://docwiki.embarcadero.com/Libraries/XE3/en/System.IOUtils.TFile.OpenRead
This is a short solution using FileSize that does the job:
function GetFileSize(p_sFilePath : string) : Int64;
var
oFile : file of Byte;
begin
Result := -1;
AssignFile(oFile, p_sFilePath);
try
Reset(oFile);
Result := FileSize(oFile);
finally
CloseFile(oFile);
end;
end;
From what I know, FileSize is available only from XE2.
uses
System.Classes, System.IOUtils;
function GetFileSize(const FileName : string) : Int64;
var
Reader: TFileStream;
begin
Reader := TFile.OpenRead(FileName);
try
result := Reader.Size;
finally
Reader.Free;
end;
end;

Resources