installer; ide: (#712) Added language selection to IDE and Installer
authorJerome St-Louis <jerome@ecere.com>
Tue, 18 Mar 2014 00:19:17 +0000 (20:19 -0400)
committerJerome St-Louis <jerome@ecere.com>
Tue, 18 Mar 2014 00:48:25 +0000 (20:48 -0400)
ide/Makefile
ide/ide.epj
ide/src/IDESettings.ec
ide/src/ide.ec
ide/src/licensing.ec
ide/src/project/Project.ec
installer/Installer.epj
installer/src/installer.ec

index 161870f..ec9c45f 100644 (file)
@@ -180,6 +180,16 @@ RESOURCES2 = \
        ../extras/res/licenses/MinGW-w64.LICENSE \
        ../extras/res/licenses/tdm-gcc.LICENSE \
        ../extras/res/licenses/ffi.LICENSE \
+       ../extras/res/types/countryCode/es.png \
+       ../extras/res/types/countryCode/gb.png \
+       ../extras/res/types/countryCode/hu.png \
+       ../extras/res/types/countryCode/cn.png \
+       ../extras/res/types/countryCode/pt.png \
+       ../extras/res/types/countryCode/vn.png \
+       ../extras/res/types/countryCode/ru.png \
+       ../extras/res/types/countryCode/in.png \
+       ../extras/res/types/countryCode/nl.png \
+       ../extras/res/types/countryCode/il.png \
        res/ecere.jpg \
        res/ecereBack.jpg \
        res/icon.png \
@@ -277,6 +287,7 @@ endif
        $(EAR) aw$(EARFLAGS) $(TARGET) locale/vi_VI/LC_MESSAGES/ide.mo "locale/vi_VI/LC_MESSAGES"
        $(EAR) aw$(EARFLAGS) $(TARGET) ../LICENSE ../extras/res/licenses/png.LICENSE ../extras/res/licenses/tango.COPYING ../extras/res/licenses/zlib.README ../extras/res/licenses/sqlite.LICENSE ../extras/res/licenses/jpg.LICENSE ../extras/res/licenses/ungif.LICENSE ../extras/res/licenses/freetype.LICENSE ../extras/res/licenses/harfbuzz.LICENSE ../extras/res/licenses/upx.LICENSE "licenses"
        $(EAR) aw$(EARFLAGS) $(TARGET) ../extras/res/licenses/MinGW-w64.LICENSE ../extras/res/licenses/tdm-gcc.LICENSE ../extras/res/licenses/ffi.LICENSE "licenses"
+       $(EAR) aw$(EARFLAGS) $(TARGET) ../extras/res/types/countryCode/es.png ../extras/res/types/countryCode/gb.png ../extras/res/types/countryCode/hu.png ../extras/res/types/countryCode/cn.png ../extras/res/types/countryCode/pt.png ../extras/res/types/countryCode/vn.png ../extras/res/types/countryCode/ru.png ../extras/res/types/countryCode/in.png ../extras/res/types/countryCode/nl.png ../extras/res/types/countryCode/il.png "countryCode"
 else
        $(AR) rcs $(TARGET) $(OBJECTS) $(LIBS)
 endif
index 912f3d8..31ad2f9 100644 (file)
             "../extras/res/licenses/ffi.LICENSE"
          ]
       },
+      {
+         "Folder" : "countryCode",
+         "Files" : [
+            "../extras/res/types/countryCode/es.png",
+            "../extras/res/types/countryCode/gb.png",
+            "../extras/res/types/countryCode/hu.png",
+            "../extras/res/types/countryCode/cn.png",
+            "../extras/res/types/countryCode/pt.png",
+            "../extras/res/types/countryCode/vn.png",
+            "../extras/res/types/countryCode/ru.png",
+            "../extras/res/types/countryCode/in.png",
+            "../extras/res/types/countryCode/nl.png",
+            "../extras/res/types/countryCode/il.png"
+         ]
+      },
       "ecere.jpg",
       "ecereBack.jpg",
       "icon.png",
index 4a68ddb..6397891 100644 (file)
@@ -1,7 +1,7 @@
 #ifdef ECERE_STATIC
-import static "ecere"
+public import static "ecere"
 #else
-import "ecere"
+public import "ecere"
 #endif
 
 import "StringsBox"
@@ -23,6 +23,31 @@ char * settingsDirectoryNames[DirTypes] =
    "Executable Files"
 };
 
+// This function cannot accept same pointer for source and output
+// todo: rename ReplaceSpaces to EscapeSpaceAndSpecialChars or something
+void ReplaceSpaces(char * output, char * source)
+{
+   int c, dc;
+   char ch, pch = 0;
+
+   for(c = 0, dc = 0; (ch = source[c]); c++, dc++)
+   {
+      if(ch == ' ') output[dc++] = '\\';
+      if(ch == '\"') output[dc++] = '\\';
+      if(ch == '&') output[dc++] = '\\';
+      if(pch != '$')
+      {
+         if(ch == '(' || ch == ')') output[dc++] = '\\';
+         pch = ch;
+      }
+      else if(ch == ')')
+         pch = 0;
+      output[dc] = ch;
+   }
+   output[dc] = '\0';
+}
+
+
 enum GlobalSettingsChange { none, editorSettings, projectOptions, compilerSettings };
 
 enum PathRelationship { unrelated, identical, siblings, subPath, parentPath, insuficientInput, pathEmpty, toEmpty, pathNull, toNull, bothEmpty, bothNull };
@@ -443,6 +468,17 @@ public:
       isset { return defaultCompiler && defaultCompiler[0]; }
    }
 
+   property String language
+   {
+      set
+      {
+         delete language;
+         language = CopyString(value);
+      }
+      get { return language; }
+      isset { return language != null; }
+   }
+
 private:
    char * docDir;
    char * ideFileDialogLocation;
@@ -451,6 +487,7 @@ private:
    char * projectDefaultIntermediateObjDir;
    char * compilerConfigsDir;
    char * defaultCompiler;
+   String language;
 
    CompilerConfig GetCompilerConfig(String compilerName)
    {
@@ -1078,3 +1115,204 @@ private:
       return copy;
    }
 }
+
+struct LanguageOption
+{
+   String name;
+   String bitmap;
+   String code;
+   BitmapResource res;
+
+   char * OnGetString(char * tempString, void * fieldData, bool * needClass)
+   {
+      return name;
+   }
+
+   void OnDisplay(Surface surface, int x, int y, int width, void * data, Alignment alignment, DataDisplayFlags flags)
+   {
+      Bitmap icon = res ? res.bitmap : null;
+      int w = 8 + 16;
+      if(icon)
+         surface.Blit(icon, x + (16 - icon.width) / 2,y+2,0,0, icon.width, icon.height);
+      class::OnDisplay(surface, x + w, y, width - w, null, alignment, flags);
+   }
+};
+
+Array<LanguageOption> languages
+{ [
+   { "English",            ":countryCode/gb.png", "" },
+   { "汉语",                ":countryCode/cn.png", "zh_CN" },
+   { "Español",            ":countryCode/es.png", "es_ES" },
+   { "Русский (43%)",      ":countryCode/ru.png", "ru_RU" },
+   { "Português (28%)",    ":countryCode/pt.png", "pt_BR" },
+   { "Nederlandse (13%)",  ":countryCode/nl.png", "nl_NL" },
+   { "Tiếng Việt (12%)",   ":countryCode/vn.png", "vi_VI" },
+   { "मराठी (10%)",          ":countryCode/in.png", "mr_MR" },
+   { "Hebrew (8%)",        ":countryCode/il.png", "he_HE" },
+   { "Magyar (8%)",        ":countryCode/hu.png", "hu_HU" }
+] };
+
+String GetLanguageString()
+{
+   String language = getenv("LANGUAGE");
+   if(!language) language = getenv("LC_ALL");
+   if(!language) language = getenv("LC_MESSAGES");
+   if(!language) language = getenv("LANG");
+   if(!language) language = "";
+   return language;
+}
+
+bool LanguageRestart(char * code, Application app, IDESettings settings, IDESettingsContainer settingsContainer, Window ide, Window projectView, bool wait)
+{
+   bool restart = true;
+   String command = null;
+   int arg0Len = strlen(app.argv[0]);
+   int len = arg0Len;
+   int j;
+   char ch;
+
+   if(ide)
+   {
+      Window w;
+
+      if(projectView)
+      {
+         Window w;
+         for(w = ide.firstChild; w; w = w.next)
+         {
+            if(w.isActiveClient && w.isDocument)
+            {
+               if(!w.CloseConfirmation(true))
+               {
+                  restart = false;
+                  break;
+               }
+            }
+         }
+         if(restart)
+         {
+            if(!projectView.CloseConfirmation(true))
+               restart = false;
+            if(projectView.fileName)
+            {
+               char * name = projectView.fileName;
+               if(name)
+               {
+                  for(j = 0; (ch = name[j]); j++)
+                     len += (ch == ' ' || ch == '\"' || ch == '&' || ch == '$' || ch == '(' || ch == ')') ? 2 : 1;
+               }
+            }
+
+            command = new char[len + 1];
+
+            strcpy(command, app.argv[0]);
+            len = arg0Len;
+            if(projectView.fileName)
+            {
+               strcat(command, " ");
+               len++;
+               ReplaceSpaces(command + len, projectView.fileName);
+            }
+         }
+         if(restart)
+         {
+            for(w = ide.firstChild; w; w = w.next)
+               if(w.isActiveClient && w.isDocument)
+                  w.modifiedDocument = false;
+            projectView.modifiedDocument = false;
+         }
+      }
+      else
+      {
+         for(w = ide.firstChild; w; w = w.next)
+         {
+            if(w.isActiveClient && w.isDocument)
+            {
+               if(!w.CloseConfirmation(true))
+               {
+                  restart = false;
+                  break;
+               }
+               if(w.fileName)
+               {
+                  char * name = w.fileName;
+                  len++;
+                  for(j = 0; (ch = name[j]); j++)
+                     len += (ch == ' ' || ch == '\"' || ch == '&' || ch == '$' || ch == '(' || ch == ')') ? 2 : 1;
+               }
+            }
+         }
+
+         if(restart)
+         {
+            command = new char[len + 1];
+            strcpy(command, app.argv[0]);
+            len = arg0Len;
+
+            for(w = ide.firstChild; w; w = w.next)
+            {
+               if(w.isActiveClient && w.isDocument)
+               {
+                  char * name = w.fileName;
+                  if(name)
+                  {
+                     strcat(command, " ");
+                     len++;
+                     ReplaceSpaces(command + len, name);
+                     len = strlen(command);
+                  }
+               }
+            }
+         }
+         if(restart)
+         {
+            for(w = ide.firstChild; w; w = w.next)
+               if(w.isActiveClient && w.isDocument)
+                  w.modifiedDocument = false;
+         }
+      }
+      if(restart)
+      {
+         settings.language = code;
+         settingsContainer.Save();
+         if(eClass_IsDerived(app._class, class(GuiApplication)))
+         {
+            GuiApplication guiApp = (GuiApplication)app;
+            guiApp.desktop.Destroy(0);
+         }
+      }
+   }
+   else
+   {
+      int i;
+      for(i = 1; i < app.argc; i++)
+      {
+         char * arg = app.argv[i];
+         len++;
+         for(j = 0; (ch = arg[j]); j++)
+            len += (ch == ' ' || ch == '\"' || ch == '&' || ch == '$' || ch == '(' || ch == ')') ? 2 : 1;
+      }
+
+      command = new char[len + 1];
+      strcpy(command, app.argv[0]);
+      len = arg0Len;
+      for(i = 1; i < app.argc; i++)
+      {
+         strcat(command, " ");
+         len++;
+         ReplaceSpaces(command + len, app.argv[i]);
+         len = strlen(command);
+      }
+   }
+
+   if(restart)
+   {
+      SetEnvironment("LANGUAGE", code);
+      if(wait)
+         ExecuteWait(command);
+      else
+         Execute(command);
+   }
+   delete command;
+   return restart;
+}
index 081ba9e..4b1df12 100644 (file)
@@ -404,7 +404,7 @@ class IDEWorkSpace : Window
    menu = Menu {  };
    IDEToolbar toolBar;
 
-   MenuItem * driverItems, * skinItems;
+   MenuItem * driverItems, * skinItems, * languageItems;
    StatusField pos { width = 150 };
    StatusField ovr, caps, num;
    DualPipe documentor;
@@ -1414,7 +1414,7 @@ class IDEWorkSpace : Window
       MenuDivider { viewMenu };
       MenuItem viewColorPicker
       {
-         viewMenu, $"Color Picker...", l, Key { c, ctrl = true , shift = true };
+         viewMenu, $"Color Picker...", c, Key { c, ctrl = true , shift = true };
          bool NotifySelect(MenuItem selection, Modifiers mods)
          {
             ColorPicker colorPicker { master = this };
@@ -1438,6 +1438,11 @@ class IDEWorkSpace : Window
       };
       */
       Menu driversMenu { viewMenu, $"Graphics Driver", v };
+
+      MenuDivider { viewMenu };
+
+      Menu languageMenu { viewMenu, $"Language", l };
+
       //Menu skinsMenu { viewMenu, "GUI Skins", k };
    Menu windowMenu { menu, $"Window", w };
       MenuItem { windowMenu, $"Close All", l, NotifySelect = MenuWindowCloseAll };
@@ -3112,10 +3117,10 @@ class IDEWorkSpace : Window
                   break;
                }
                else
-                  ide.OpenFile(fullPath, app.argc > 2, true, openAsText ? "txt" : null, yes, normal, false);
+                  ide.OpenFile(fullPath, app.argFilesCount > 1, true, openAsText ? "txt" : null, yes, normal, false);
             }
             else if(strstr(fullPath, "http://") == fullPath)
-               ide.OpenFile(fullPath, app.argc > 2, true, openAsText ? "txt" : null, yes, normal, false);
+               ide.OpenFile(fullPath, app.argFilesCount > 1, true, openAsText ? "txt" : null, yes, normal, false);
          }
       }
       if(passThrough && projectView && projectView.project && workspace)
@@ -3620,6 +3625,7 @@ class IDEApp : GuiApplication
    //skin = "TVision";
 
    TempFile includeFile { };
+   int argFilesCount;
 
    bool Init()
    {
@@ -3628,7 +3634,38 @@ class IDEApp : GuiApplication
       //SetLoggingMode(debug, null);
 
       settingsContainer.Load();
-      if(argc > 1 && !strcmpi(GetExtension(argv[1], ext), "3ds"))
+      {
+         String language = GetLanguageString();
+         if(ideSettings.language.OnCompare(language))
+         {
+            LanguageRestart(ideSettings.language, app, null, null, null, null, true);
+            return false;
+         }
+      }
+
+      // First count files arg to decide whether to maximize
+      {
+         bool passThrough = false, debugWorkDir = false;
+         int c;
+         argFilesCount = 0;
+         for(c = 1; c<app.argc; c++)
+         {
+            if(passThrough);
+            else if(debugWorkDir)
+               debugWorkDir = false;
+            else if(!strcmp(app.argv[c], "-t"));
+            else if(!strcmp(app.argv[c], "-no-parsing"));
+            else if(!strcmp(app.argv[c], "-debug-start"));
+            else if(!strcmp(app.argv[c], "-debug-work-dir"))
+               debugWorkDir = true;
+            else if(!strcmp(app.argv[c], "-@"))
+               passThrough = true;
+            else
+               argFilesCount++;
+         }
+      }
+
+      if(app.argFilesCount > 1 && !strcmpi(GetExtension(argv[1], ext), "3ds"))
       {
          app.driver = "OpenGL";
          ide.driverItems[1].checked = true;
@@ -3653,10 +3690,20 @@ class IDEApp : GuiApplication
          char fullPath[MAX_LOCATION];
          GetWorkingDir(fullPath, MAX_LOCATION);
          PathCat(fullPath, app.argv[c]);
-         ide.OpenFile(fullPath, app.argc > 2, true, null, yes, normal, false);
+         ide.OpenFile(fullPath, app.argFilesCount > 1, true, null, yes, normal, false);
       }
       */
 
+      // Default to language specified by environment if no language selected
+      if(!ideSettings.language)
+      {
+         String language = GetLanguageString();
+         if(language)
+            ideSettings.language = language;
+         else
+            ideSettings.language = "";
+      }
+
       // Default to home directory if no directory yet set up
       if(!ideSettings.ideProjectFileDialogLocation[0])
       {
@@ -3700,8 +3747,62 @@ class IDEApp : GuiApplication
       if(!LoadIncludeFile())
          PrintLn("error: unable to load :crossplatform.mk file inside ide binary.");
 
+      // Create language menu
+      {
+         String language = ideSettings.language;
+         int i = 0;
+
+         ide.languageItems = new MenuItem[languages.count];
+         for(l : languages)
+         {
+            ide.languageItems[i] =
+            {
+               ide.languageMenu, l.name;
+               bitmap = { l.bitmap };
+               id = i;
+               isRadio = true;
+
+               bool Window::NotifySelect(MenuItem selection, Modifiers mods)
+               {
+                  if(!LanguageRestart(languages[(int)selection.id].code, app, ideSettings, settingsContainer, ide, ide.projectView, false))
+                  {
+                     // Re-select previous selected language if aborted
+                     String language = ideSettings.language;
+                     int i = 0;
+                     for(l : languages)
+                     {
+                        if(((!language || !language[0]) && i == 0) ||
+                           (language && !strcmpi(l.code, language)))
+                        {
+                           ide.languageItems[i].checked = true;
+                           break;
+                        }
+                        i++;
+                     }
+                  }
+                  return true;
+               }
+            };
+            if(((!language || !language[0]) && i == 0) ||
+               (language && !strcmpi(l.code, language)))
+               ide.languageItems[i].checked = true;
+            i++;
+         }
+         MenuDivider { ide.languageMenu };
+         MenuItem
+         {
+            ide.languageMenu, $"Help Translate";
+
+            bool Window::NotifySelect(MenuItem selection, Modifiers mods)
+            {
+               ShellOpen("http://translations.launchpad.net/ecere");
+               return true;
+            }
+         };
+      }
+
       ideMainFrame.Create();
-      if(app.argc > 2)
+      if(app.argFilesCount > 1)
          ide.MenuWindowTileVert(null, 0);
       return true;
    }
index ff880e9..fc68d66 100644 (file)
@@ -151,7 +151,7 @@ class LicensesForm : Window
    {
       this;
       caption = $"I don't agree";
-      size = { 100, 22 };
+      minClientSize = { 100, 20 };
       anchor = { bottom = 10, right = 14 };
 
       bool NotifyClicked(Button button, int x, int y, Modifiers mods)
@@ -166,7 +166,7 @@ class LicensesForm : Window
       caption = $"I agree";
       font = { "Verdana", 10, bold = true };
       isDefault = true;
-      size = { 80, 23 };
+      minClientSize = { 80, 21 };
       anchor = { bottom = 10 };
       NotifyClicked = ButtonCloseDialog;
    };
index 1af23e1..2ab285c 100644 (file)
@@ -414,30 +414,6 @@ define ProjectExtension = "epj";
 define stringInFileIncludedFrom = "In file included from ";
 define stringFrom =               "                 from ";
 
-// This function cannot accept same pointer for source and output
-// todo: rename ReplaceSpaces to EscapeSpaceAndSpecialChars or something
-void ReplaceSpaces(char * output, char * source)
-{
-   int c, dc;
-   char ch, pch = 0;
-
-   for(c = 0, dc = 0; (ch = source[c]); c++, dc++)
-   {
-      if(ch == ' ') output[dc++] = '\\';
-      if(ch == '\"') output[dc++] = '\\';
-      if(ch == '&') output[dc++] = '\\';
-      if(pch != '$')
-      {
-         if(ch == '(' || ch == ')') output[dc++] = '\\';
-         pch = ch;
-      }
-      else if(ch == ')')
-         pch = 0;
-      output[dc] = ch;
-   }
-   output[dc] = '\0';
-}
-
 // chars refused for project name: windowsFileNameCharsNotAllowed + " &,."
 
 //define gnuMakeCharsNeedEscaping = "$%";
index 468fa0e..7c9bde2 100644 (file)
             }
          ]
       },
+      {
+         "Folder" : "countryCode",
+         "Files" : [
+            "../extras/res/types/countryCode/es.png",
+            "../extras/res/types/countryCode/gb.png",
+            "../extras/res/types/countryCode/hu.png",
+            "../extras/res/types/countryCode/cn.png",
+            "../extras/res/types/countryCode/pt.png",
+            "../extras/res/types/countryCode/vn.png",
+            "../extras/res/types/countryCode/ru.png",
+            "../extras/res/types/countryCode/in.png",
+            "../extras/res/types/countryCode/nl.png",
+            "../extras/res/types/countryCode/il.png"
+         ]
+      },
       "../ide/res/icon.png",
       "ecere.png",
       "ryoanji.png",
index 1477b57..44ff09b 100644 (file)
@@ -1,7 +1,7 @@
 #ifdef NOMINGW
-static define buildString = $"Ecere SDK v0.44.10 (Without MinGW) -- built on March 2nd, 2014 ";
+static define buildString = $"Ecere SDK v0.44.10 (Without MinGW) -- built on March 9th, 2014 ";
 #else
-static define buildString = $"Ecere SDK v0.44.10 -- built on March 2nd, 2014 ";
+static define buildString = $"Ecere SDK v0.44.10 -- built on March 9th, 2014 ";
 #endif
 
 #define WIN32_LEAN_AND_MEAN
@@ -19,6 +19,8 @@ import "createLink"
 import "licensing"
 import "CheckListBox"
 
+static LanguageOption dummy; // TOFIX
+
 static bool IsAdministrator()
 {
    bool b;
@@ -430,15 +432,17 @@ bool osIS64bit;
 
 class Installer : Window
 {
-   text = $"Ecere Software Development Kit Setup - v0.44.10 \"Ryōan-ji\" 64 Bit Edition";
-   background = activeBorder;
+   background = formColor;
    borderStyle = fixed;
    hasMinimize = true;
    hasClose = true;
    tabCycle = true;
    clientSize = { 636, 476 };
-   // clientSize = { 796, 576 };
    icon = { ":icon.png" };
+   text = $"Ecere Software Development Kit Setup - v0.44.10 \"Ryōan-ji\" 64 Bit Edition";
+
+   // clientSize = { 796, 576 };
+   bool loaded;
 
    Picture back { image = BitmapResource { ":ryoanji.png" }, parent = this, position = { 0, 0 } };
    FileDialog fileDialog
@@ -574,12 +578,12 @@ class Installer : Window
          }
       }
    };
-   Label agreementLbl { parent = this, text = $"By installing the Ecere SDK, you agree to the                                         .", font = { "Tahoma", 8.25f }, anchor = Anchor { left = 24, top = 444 } };
+   Label agreementLbl { parent = this, text = $"By installing the Ecere SDK, you agree to the", font = { "Tahoma", 8.25f }, anchor = Anchor { right = 399, top = 448 } };
    Button licenseButton
    {
       this, inactive = true, offset = false, bevel = false, foreground = blue, font = { "Tahoma", 8.25f, underline = true, bold = true },
       // text = $"terms and conditions", anchor = Anchor { left = 241, top = 421 };
-      text = $"terms and conditions", anchor = Anchor { left = 237, top = 441 };
+      text = $"terms and conditions", anchor = Anchor { left = 235, top = 445 };
       cursor = ((GuiApplication)__thisModule).GetCursor(hand);
 
       bool NotifyClicked(Button button, int x, int y, Modifiers mods)
@@ -589,6 +593,7 @@ class Installer : Window
          return true;
       }
    };
+   Label dotLbl { parent = this, text = ".", font = { "Tahoma", 8.25f }, anchor = Anchor { left = 372, top = 448 } };
    CheckListBox optionsBox
    {
       this, size = { 460, 114 }, position = { 160, 284 };
@@ -660,7 +665,7 @@ class Installer : Window
    };
    Button install
    {
-      parent = this, text = $"Install", isDefault = true, size = { 75, 23 }, position = { 432, 436 };
+      parent = this, text = $"Install", isDefault = true, size = { 75, 23 }, position = { 432, 440 };
 
       bool NotifyClicked(Button button, int x, int y, Modifiers mods)
       {
@@ -671,7 +676,39 @@ class Installer : Window
          return true;
       }
    };
-   Button button3 { parent = this, text = $"Cancel", hotKey = altX, size = Size { 75, 23 }, anchor = Anchor { left = 544, top = 436 }, NotifyClicked = ButtonCloseDialog };
+   Button button3 { parent = this, text = $"Cancel", hotKey = altX, size = Size { 75, 23 }, anchor = Anchor { left = 544, top = 440 }, NotifyClicked = ButtonCloseDialog };
+   DropBox languageBox
+   {
+      this, position = { 14, 374 }, size = { 142, 0 }, caption = "Language:";
+
+      bool NotifySelect(DropBox dropBox, DataRow row, Modifiers mods)
+      {
+         LanguageOption * option = row.GetData(null);
+         // If the language is already set, we need to override it
+         {
+            IDESettings settings = null;
+            IDESettingsContainer settingsContainer
+            {
+               driver = "JSON";
+               dataOwner = &settings;
+               dataClass = class(IDESettings);
+               allUsers = options[0].selected;
+            };
+            settingsContainer.Load();
+            if(settings.language)
+            {
+               settings.language = option->code;
+               settingsContainer.Save();
+            }
+            delete settingsContainer;
+            delete settings;
+         }
+         ((GuiApplication)__thisModule.application).desktop.Destroy(0);
+         LanguageRestart(option->code, __thisModule.application, null, null, null, null, false);
+         return true;
+      }
+   };
+   Label lblLanguageBox { this, position = { 14, 354 }, labeledWindow = languageBox };
    Label label1 { labeledWindow = destBox, tabCycle = true, isGroupBox = true, parent = this, inactive = false, size = Size { 458, 50 }, anchor = Anchor { left = 160, top = 96 } };
    PathBox destBox
    {
@@ -733,7 +770,7 @@ class Installer : Window
    };
    EditBox label7
    {
-      this, opacity = 0, borderStyle = none, inactive = true, size = { 136, 53 }, position = { 14, 280 }, noSelect = true,
+      this, opacity = 0, borderStyle = none, inactive = true, size = { 136, 83 }, position = { 14, 280 }, noSelect = true,
       multiLine = true,
       contents = $"Select icons to install, file\n"
       "associations, and system\n"
@@ -741,20 +778,18 @@ class Installer : Window
    };
    Label totalSpaceLabel
    {
-      this, position = { 18, 352 }, text = $"Space Required: "
+      this, anchor = { right = 72, top = 404 }, text = $"Space Required: "
    };
    Label totalSpaceValue
    {
-      this, position = { 100, 352 }, text = "0 mb"
+      this, anchor = { right = 14, top = 404 }, text = "0 mb"
    };
    EditBox editBox1
    {
-      inactive = true, noSelect = true,
-      multiLine = true, parent = label3, text = "editBox1", opacity = 0, borderStyle = none, size = Size { 350, 35 }, anchor = Anchor { horz = 111, vert = 13 },
-      contents = $"Choose in which folder to install the Ecere SDK, which features\n"
+      label3, caption = "editBox1", opacity = 0, borderStyle = none, inactive = true, size = { 350, 35 }, position = { 256, 40 }, multiLine = true, noSelect = true, contents = $"Choose in which folder to install the Ecere SDK, which features\n"
          "of the SDK to install, as well as where to install program icons."
    };
-   Label label2 { parent = this, text = buildString, position = { 16, 412 }, font = { "Tahoma", 10, true }, disabled = true, opacity = 0, background = activeBorder };
+   Label label2 { parent = this, text = buildString, position = { 16, 422 }, font = { "Tahoma", 10, true }, disabled = true, opacity = 0, background = activeBorder };
    Picture picture1
    {
       image = BitmapResource { ":ecere.png", alphaBlend = true }, filter = true, parent = label3, text = "picture1", anchor = Anchor { left = 16, top = 4 };
@@ -766,13 +801,15 @@ class Installer : Window
          return true;
       }
    };
-   Label label4 { parent = label3, text = $"Choose Components, Locations and Install Options", font = FontResource { "Tahoma", 8.25f, bold = true }, size = Size { 326, 16 }, anchor = Anchor { horz = 91, vert = -12 } };
+   Label label4 { label3, font = { "Tahoma", 8.25f, bold = true }, /*size = { 326, 16 }, */position = { 248, 24 }, text = $"Choose Components, Locations and Install Options" };
    DataField componentField { "CheckItem", width = 160, header = $"Component" };
    DataField locationField { "char *", width = 108, header = $"Destination Folder", editable = true };
    DataField reqField { "FileSize", width = 70, header = $"Req. Space", alignment = right };
    DataField avField { "FileSize64", width = 70, header = $"Avail. Space", alignment = right };
    DataField optionField { "CheckItem" };
 
+   DataField languageField { class(LanguageOption) };
+
    void SetAvailableSpace(Component component, char * parentPath)
    {
       char path[MAX_LOCATION];
@@ -920,6 +957,33 @@ class Installer : Window
          options[0].selected = false;
       }
 
+      // If the SDK is already installed, use currently selected language
+      {
+         IDESettings settings = null;
+         IDESettingsContainer settingsContainer
+         {
+            driver = "JSON";
+            dataOwner = &settings;
+            dataClass = class(IDESettings);
+            allUsers = options[0].selected;
+         };
+
+         settingsContainer.Load();
+
+         if(settings.language)
+         {
+            String language = GetLanguageString();
+            if(settings.language.OnCompare(language))
+            {
+               // Relaunch the installer with previously selected language
+               LanguageRestart(settings.language, __thisModule.application, null, null, null, null, false);
+               return false;
+            }
+         }
+         delete settingsContainer;
+         delete settings;
+      }
+
       GetEnvironment("HOMEDRIVE", homeDrive, sizeof(homeDrive));
       GetEnvironment("windir", winDir, sizeof(winDir));
 
@@ -932,6 +996,8 @@ class Installer : Window
 
       optionsBox.AddField(optionField);
 
+      languageBox.AddField(languageField);
+
       programFilesDir[0] = 0;
       if(GetEnvironment("ProgramFiles", programFilesDir, MAX_LOCATION))
       {
@@ -1018,8 +1084,43 @@ class Installer : Window
       }
    }
 
+   void OnDestroy()
+   {
+      for(l : languages)
+         delete l.res;
+   }
+
+   bool OnPostCreate()
+   {
+      dotLbl.position.x = licenseButton.position.x + licenseButton.size.w - 4;
+      return true;
+   }
+
    bool OnCreate()
    {
+      // Constructor happens before Languages is instantiated...
+      for(l : languages)
+      {
+         l.res = { l.bitmap, window = this };
+         incref l.res;
+      }
+
+      if(!loaded)
+      {
+         String language = GetLanguageString();
+         for(l : languages)
+         {
+            LanguageOption option = l;
+            DataRow row = languageBox.AddRow();
+            row.SetData(null, option); // TOFIX: l used directly here
+            if(!language.OnCompare(l.code))
+               languageBox.currentRow = row;
+         }
+         if(!languageBox.currentRow)
+            languageBox.currentRow = languageBox.firstRow;
+         loaded = true;
+      }
+
       destBox.Activate();
       return true;
    }
@@ -1028,12 +1129,11 @@ class Installer : Window
    {
       int tw = label2.size.w;
       surface.SetForeground(Color { 128, 128, 128 });
-      surface.HLine(label2.position.x + tw + 6, 620, 420);
+      surface.HLine(label2.position.x + tw + 6, 620, 430);
       surface.SetForeground(white);
-      surface.HLine(label2.position.x + tw + 6, 621, 421);
+      surface.HLine(label2.position.x + tw + 6, 621, 431);
       surface.PutPixel(621, 400);
    }
-
    Label label3
    {
       parent = this, opacity = 0, borderStyle = deep, size = Size { 644, 93 }, anchor = Anchor { left = -8, top = -8 };
@@ -1058,13 +1158,13 @@ class InstallProgress : Window
    ProgressBar progressBar { parent = this, size = Size { 588, 24 }, anchor = Anchor { left = 24, top = 184 } };
    Button finish
    {
-      parent = this, text = $"Install", disabled = true, isDefault = true, size = Size { 75, 23 }, anchor = Anchor { left = 432, top = 436 };
+      parent = this, text = $"Install", disabled = true, isDefault = true, size = Size { 75, 23 }, anchor = Anchor { left = 432, top = 440 };
 
       NotifyClicked = ButtonCloseDialog
    };
    Button cancel
    {
-      this, text = $"Cancel", hotKey = altX, size = Size { 75, 23 }, anchor = Anchor { left = 544, top = 436 };
+      this, text = $"Cancel", hotKey = altX, size = Size { 75, 23 }, anchor = Anchor { left = 544, top = 440 };
 
       bool NotifyClicked(Button button, int x, int y, Modifiers mods)
       {
@@ -1078,7 +1178,7 @@ class InstallProgress : Window
       multiLine = true, parent = label3, opacity = 0, borderStyle = none, size = Size { 350, 35 }, anchor = Anchor { horz = 111, vert = 13 },
       contents = $"Please wait while the Ecere Software Development Kit is being installed."
    };
-   Label label2 { parent = this, text = buildString, position = { 16, 412 }, font = { "Tahoma", 10, true }, disabled = true, opacity = 0, background = activeBorder };
+   Label label2 { parent = this, text = buildString, position = { 16, 422 }, font = { "Tahoma", 10, true }, disabled = true, opacity = 0, background = activeBorder };
    Picture picture1
    {
       image = BitmapResource { ":ecere.png", alphaBlend = true }, filter = true, parent = label3, anchor = Anchor { left = 16, top = 4 };
@@ -1096,9 +1196,9 @@ class InstallProgress : Window
    {
       int tw = label2.size.w;
       surface.SetForeground(Color { 128, 128, 128 });
-      surface.HLine(label2.position.x + tw + 6, 620, 420);
+      surface.HLine(label2.position.x + tw + 6, 620, 430);
       surface.SetForeground(white);
-      surface.HLine(label2.position.x + tw + 6, 621, 421);
+      surface.HLine(label2.position.x + tw + 6, 621, 431);
       surface.PutPixel(621, 400);
    }
 
@@ -1360,6 +1460,8 @@ class InstallThread : Thread
             }
          }
 
+         settings.language = GetLanguageString();
+
          settingsContainer.Save();
          delete settingsContainer;
          delete settings;