Localize

Description

Localize is a simple localization tool, from Pascal Pignard, to help you to edit localization files.

On the same screen you see the master language text and the locale language text for each ressource key. It is thus easy to translate the text.

Supported formats:

  • UTF-16 strings files are supported for macOS app localization;
  • Latin-1 properties files are supported for Zanyblue app localization.

General notes:

  • The localization data must be valid, the program doesn’t make any consistency check;
  • Localization comment is put just the line before the related key;
    Multi-line comments are gathered in one;
  • The properties written in file are in alphabetic order of keys with = separator and LF line terminator;
  • If two properties have the same key then only the last one is kept.

Specific notes for strings files:

  • Processed escaped sequences: \t, \n, \LF, \ », \\, others escaped characters are not processed and written as it is.
  • Specific notes for properties files with Zanyblue:
  • Only characters valid for Ada identifiers are valid for keys;
  • Simple quote must be written twice;
  • Processed escaped sequences: \t, \n, \LF, \u, \ , \:, \=, \#, \!, \\, others escaped characters are not processed and written as it is.

Todo (from the author):

  • Add more tooltips;
  • Add Google translation support;
  • Localize the program itself 🙂

Source code

localize.ads

-------------------------------------------------------------------------------
-- NAME (specification)         : Localize.ads
-- AUTHOR                       : Pascal Pignard
-- ROLE                         : Root unit.
-- NOTES                        : Ada 2012, GNOGA 2.1 alpha
--
-- COPYRIGHT                    : (c) Pascal Pignard 2021
-- LICENCE                      : CeCILL V2 (http://www.cecill.info)
-- CONTACT                      : http://blady.pagesperso-orange.fr
-------------------------------------------------------------------------------

with UXStrings;

package Localize is
   use UXStrings;
   subtype String is UXString;
end Localize;

localize-view.ads

-------------------------------------------------------------------------------
-- NAME (specification)         : localize-view.ads
-- AUTHOR                       : Pascal Pignard
-- ROLE                         : User interface display unit.
-- NOTES                        : Ada 2012, GNOGA 2.1 alpha
--
-- COPYRIGHT                    : (c) Pascal Pignard 2021
-- LICENCE                      : CeCILL V2 (http://www.cecill.info)
-- CONTACT                      : http://blady.pagesperso-orange.fr
-------------------------------------------------------------------------------

with Gnoga.Gui.Base;
with Gnoga.Gui.View.Grid;
with Gnoga.Gui.Element.Common;
with Gnoga.Gui.Element.Form;
with Gnoga.Gui.Window;

with Localize.Parser;

package Localize.View is

   type Default_View_Type is new Gnoga.Gui.View.Grid.Grid_View_Type with record
      Main_Window      : Gnoga.Gui.Window.Pointer_To_Window_Class;
      List_Form        : Gnoga.Gui.Element.Form.Form_Type;
      Locale_List      : Gnoga.Gui.Element.Form.Selection_Type;
      Master_Path      : Gnoga.Gui.Element.Form.Text_Type;
      Locale_Path      : Gnoga.Gui.Element.Form.Text_Type;
      Load_Button      : Gnoga.Gui.Element.Form.Input_Button_Type;
      H1, H2, H3       : Gnoga.Gui.Element.Common.HR_Type;
      Keys_Label       : Gnoga.Gui.Element.Common.DIV_Type;
      Key_List         : Gnoga.Gui.Element.Form.Selection_Type;
      Save_Button      : Gnoga.Gui.Element.Form.Input_Button_Type;
      Text_Form        : Gnoga.Gui.Element.Form.Form_Type;
      Select_Pattern   : Gnoga.Gui.Element.Form.Search_Type;
      Key_Input        : Gnoga.Gui.Element.Form.Text_Type;
      Duplicate_Button : Gnoga.Gui.Element.Form.Input_Button_Type;
      Insert_Button    : Gnoga.Gui.Element.Form.Input_Button_Type;
      Delete_Button    : Gnoga.Gui.Element.Form.Input_Button_Type;
      Rename_Button    : Gnoga.Gui.Element.Form.Input_Button_Type;
      Error_Label      : Gnoga.Gui.Element.Common.DIV_Type;
      Master_Text      : Gnoga.Gui.Element.Form.Text_Area_Type;
      Master_Comment   : Gnoga.Gui.Element.Form.Text_Area_Type;
      Use_Button       : Gnoga.Gui.Element.Form.Input_Button_Type;
      Locale_Text      : Gnoga.Gui.Element.Form.Text_Area_Type;
      Locale_Comment   : Gnoga.Gui.Element.Form.Text_Area_Type;
      Exit_Button      : Gnoga.Gui.Element.Form.Input_Button_Type;
      Quit_Button      : Gnoga.Gui.Element.Form.Input_Button_Type;
      Old_Key_Index    : Natural;
      Master, Locale   : Localize.Parser.Property_List;
   end record;
   type Default_View_Access is access all Default_View_Type;
   type Pointer_to_Default_View_Class is access all Default_View_Type'Class;

   overriding procedure Create
     (Grid        : in out Default_View_Type;
      Parent      : in out Gnoga.Gui.Base.Base_Type'Class;
      Layout      : in     Gnoga.Gui.View.Grid.Grid_Rows_Type;
      Fill_Parent : in     Boolean := True;
      Set_Sizes   : in     Boolean := True;
      ID          : in     String  := "");

end Localize.View;

localize-view.adb

-------------------------------------------------------------------------------
-- NAME (body)                  : localize-view.adb
-- AUTHOR                       : Pascal Pignard
-- ROLE                         : User interface display unit.
-- NOTES                        : Ada 2012, GNOGA 2.1 alpha
--
-- COPYRIGHT                    : (c) Pascal Pignard 2021
-- LICENCE                      : CeCILL V2 (http://www.cecill.info)
-- CONTACT                      : http://blady.pagesperso-orange.fr
-------------------------------------------------------------------------------

package body Localize.View is

   ------------
   -- Create --
   ------------

   overriding procedure Create
     (Grid        : in out Default_View_Type;
      Parent      : in out Gnoga.Gui.Base.Base_Type'Class;
      Layout      : in     Gnoga.Gui.View.Grid.Grid_Rows_Type;
      Fill_Parent : in     Boolean := True;
      Set_Sizes   : in     Boolean := True;
      ID          : in     String  := "")
   is
   begin
      Gnoga.Gui.View.Grid.Grid_View_Type (Grid).Create (Parent, Layout, Fill_Parent, Set_Sizes, ID);
      Grid.Panel (1, 1).Border;

      Grid.List_Form.Create (Grid.Panel (1, 1).all);
      Grid.List_Form.Put ("Master strings file path:");
      Grid.Master_Path.Create (Grid.List_Form, 40);
      Grid.List_Form.New_Line;
      Grid.List_Form.Put ("Locale strings file path:");
      Grid.Locale_Path.Create (Grid.List_Form, 40);
      Grid.Load_Button.Create (Grid.List_Form, "Load both strings files");
      Grid.H1.Create (Grid.List_Form);
      Grid.Keys_Label.Create (Grid.List_Form, "Keys (?):");
      Grid.Key_List.Create (Grid.List_Form, Visible_Lines => 20);
      Grid.List_Form.New_Line;
      Grid.List_Form.Put_Line ("(# Master key only, @ Locale key only, * Locale key modified)");
      Grid.Save_Button.Create (Grid.List_Form, "Save locale strings file");
      Grid.List_Form.New_Line;
      Grid.Exit_Button.Create (Grid.List_Form, "Disconnect");
      Grid.List_Form.New_Line;
      Grid.Quit_Button.Create (Grid.List_Form, "End Localize server");

      Grid.Panel (1, 2).Border;
      Grid.Text_Form.Create (Grid.Panel (1, 2).all);
      Grid.Text_Form.Put ("Select keys: ");
      Grid.Select_Pattern.Create (Grid.Text_Form);
      Grid.Text_Form.New_Line;
      Grid.Text_Form.Put ("Key: ");
      Grid.Key_Input.Create (Grid.Text_Form);
      Grid.Text_Form.New_Line;
      Grid.Duplicate_Button.Create (Grid.Text_Form, "Duplicate");
      Grid.Duplicate_Button.Advisory_Title ("Duplicate from master to locale");
      Grid.Insert_Button.Create (Grid.Text_Form, "Insert");
      Grid.Delete_Button.Create (Grid.Text_Form, "Delete");
      Grid.Rename_Button.Create (Grid.Text_Form, "Rename");
      Grid.Text_Form.New_Line;
      Grid.Error_Label.Create (Grid.Text_Form, "No error.");
      Grid.H2.Create (Grid.Text_Form);
      Grid.Text_Form.Put_Line ("Master text:");
      Grid.Master_Text.Create (Grid.Text_Form, 40, 4);
      Grid.Master_Text.Read_Only;
      Grid.Text_Form.Put_Line ("Locale text:");
      Grid.Locale_Text.Create (Grid.Text_Form, 40, 4);
      Grid.H3.Create (Grid.Text_Form);
      Grid.Text_Form.Put_Line ("Master comment:");
      Grid.Master_Comment.Create (Grid.Text_Form, 40, 4);
      Grid.Master_Comment.Read_Only;
      Grid.Text_Form.Put_Line ("Locale comment:");
      Grid.Locale_Comment.Create (Grid.Text_Form, 40, 4);
   end Create;

end Localize.View;

localize-controller.ads

-------------------------------------------------------------------------------
-- NAME (specification)         : localize-controller.ads
-- AUTHOR                       : Pascal Pignard
-- ROLE                         : User interface control unit.
-- NOTES                        : Ada 2012, GNOGA 2.1 alpha
--
-- COPYRIGHT                    : (c) Pascal Pignard 2021
-- LICENCE                      : CeCILL V2 (http://www.cecill.info)
-- CONTACT                      : http://blady.pagesperso-orange.fr
-------------------------------------------------------------------------------

with Gnoga.Gui.Window;
with Gnoga.Application.Multi_Connect;

package Localize.Controller is
   procedure Default
     (Main_Window : in out Gnoga.Gui.Window.Window_Type'Class;
      Connection  :        access Gnoga.Application.Multi_Connect.Connection_Holder_Type);
end Localize.Controller;

localize-controller.adb

-------------------------------------------------------------------------------
-- NAME (body)                  : localize-controller.adb
-- AUTHOR                       : Pascal Pignard
-- ROLE                         : User interface control unit.
-- NOTES                        : Ada 2012, GNOGA 2.1 alpha
--
-- COPYRIGHT                    : (c) Pascal Pignard 2021
-- LICENCE                      : CeCILL V2 (http://www.cecill.info)
-- CONTACT                      : http://blady.pagesperso-orange.fr
-------------------------------------------------------------------------------

with Gnoga.Gui.Base;
with Gnoga.Gui.View.Grid;

with Localize.View;
with Localize.Parser;

package body Localize.Controller is

   --  Handlers
   procedure On_Exit (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Quit (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Load (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Save (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Change_Key (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Change_Text (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Change_Comment (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Select (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Enter (Object : in out Gnoga.Gui.Base.Base_Type'Class) is null;
   procedure On_Duplicate (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Insert (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Delete (Object : in out Gnoga.Gui.Base.Base_Type'Class);
   procedure On_Rename (Object : in out Gnoga.Gui.Base.Base_Type'Class);

   function Modified
     (Properties : Localize.Parser.Property_List;
      Key        : String)
      return Character is (if Localize.Parser.Modified (Properties, Key) then '*' else ' ');
   function Not_In
     (Properties : Localize.Parser.Property_List;
      Key        : String;
      Tag        : Character)
      return Character is (if not Localize.Parser.Contains (Properties, Key) then Tag else ' ');

   procedure On_Exit (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Dummy_Last_View : Gnoga.Gui.View.View_Type;
   begin
      View.Remove;
      Dummy_Last_View.Create (View.Main_Window.all);
      Dummy_Last_View.Put_Line ("Disconnected!");
      View.Main_Window.Close;
      View.Main_Window.Close_Connection;
   end On_Exit;

   procedure On_Quit (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Dummy_Last_View : Gnoga.Gui.View.View_Type;
   begin
      View.Remove;
      Dummy_Last_View.Create (View.Main_Window.all);
      Dummy_Last_View.Put_Line ("Localize server ended!");
      Gnoga.Application.Multi_Connect.End_Application;
   end On_Quit;

   procedure On_Change_Key (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Key : constant String := View.Key_List.Value;
   begin
      View.Error_Label.Text ("No error.");
      View.Key_Input.Value (Key);
      View.Master_Text.Value (Localize.Parser.Text (View.Master, Key));
      View.Master_Comment.Value (Localize.Parser.Comment (View.Master, Key));
      View.Locale_Text.Value (Localize.Parser.Text (View.Locale, Key));
      View.Locale_Comment.Value (Localize.Parser.Comment (View.Locale, Key));
      View.Old_Key_Index := View.Key_List.Selected_Index;
   exception
      when others =>
         View.Error_Label.Text ("Error CK with key: " & Key);
   end On_Change_Key;

   procedure On_Change_Text (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Key_Index : constant Natural := View.Old_Key_Index;
      Key       : constant String  := (if Key_Index > 0 then View.Key_List.Value (Key_Index) else "");
   begin
      View.Error_Label.Text ("No error.");
      Localize.Parser.Text (View.Locale, Key, View.Locale_Text.Value);
      if Localize.Parser.Modified (View.Locale, Key) then
         View.Key_List.Text
           (Key_Index,
            From_Latin_1
              (Not_In (View.Master, Key, '@') & Not_In (View.Locale, Key, '#') & Modified (View.Locale, Key)) &
            Key);
      end if;
   exception
      when others =>
         View.Error_Label.Text ("Error CT with key: " & Key);
   end On_Change_Text;

   procedure On_Change_Comment (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Key_Index : constant Natural := View.Old_Key_Index;
      Key       : constant String  := (if Key_Index > 0 then View.Key_List.Value (Key_Index) else "");
   begin
      View.Error_Label.Text ("No error.");
      Localize.Parser.Comment (View.Locale, Key, View.Locale_Comment.Value);
      if Localize.Parser.Modified (View.Locale, Key) then
         View.Key_List.Text
           (Key_Index,
            From_Latin_1
              (Not_In (View.Master, Key, '@') & Not_In (View.Locale, Key, '#') & Modified (View.Locale, Key)) &
            Key);
      end if;
   exception
      when others =>
         View.Error_Label.Text ("Error CC with key: " & Key);
   end On_Change_Comment;

   procedure On_Load (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
   begin
      View.Error_Label.Text ("No error.");
      Localize.Parser.Read (View.Master, View.Master_Path.Value);
      Localize.Parser.Read (View.Locale, View.Locale_Path.Value);
      On_Select (Object);
   exception
      when others =>
         View.Error_Label.Text ("Error with files.");
   end On_Load;

   procedure On_Save (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Key : constant String := View.Key_List.Value;
   begin
      View.Error_Label.Text ("No error.");
      Localize.Parser.Write (View.Locale, View.Locale_Path.Value);
      Localize.Parser.Reset_Modified_Indicators (View.Locale);
      On_Select (Object);
   exception
      when others =>
         View.Error_Label.Text ("Error with file or key: " & Key);
   end On_Save;

   procedure On_Select (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Old_Key : constant String := View.Key_List.Value;
   begin
      View.Error_Label.Text ("No error.");
      View.Old_Key_Index := 0;
      View.Key_List.Empty_Options;
      for Key of Localize.Parser.Selected_Keys (View.Master, View.Locale, View.Select_Pattern.Value) loop
         View.Key_List.Add_Option
           (Key,
            From_Latin_1
              (Not_In (View.Master, Key, '@') & Not_In (View.Locale, Key, '#') & Modified (View.Locale, Key)) &
            Key);
         if Key = Old_Key then
            View.Key_List.Selected (View.Key_List.Length);
         end if;
      end loop;
      View.Keys_Label.Text ("Keys (" & Gnoga.Image (View.Key_List.Length) & "):");
      if View.Key_List.Length > 0 then
         if View.Key_List.Selected_Index = 0 then
            View.Key_List.Selected (1);
         end if;
         View.Key_List.Focus;
      end if;
      On_Change_Key (Object);
   exception
      when others =>
         View.Error_Label.Text ("Error S with key: " & Old_Key);
   end On_Select;

   procedure On_Duplicate (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Key : constant String := View.Key_Input.Value;
   begin
      if Key /= "" then
         View.Error_Label.Text ("No error.");
         Localize.Parser.Text (View.Locale, Key, Localize.Parser.Text (View.Master, Key));
         Localize.Parser.Comment (View.Locale, Key, Localize.Parser.Comment (View.Master, Key));
         View.Locale_Text.Value (Localize.Parser.Text (View.Master, Key));
         View.Locale_Comment.Value (Localize.Parser.Comment (View.Master, Key));
         On_Select (Object);
      else
         View.Error_Label.Text ("Empty key.");
      end if;
   exception
      when others =>
         View.Error_Label.Text ("Error with key: " & Key);
   end On_Duplicate;

   procedure On_Insert (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Key : constant String := View.Key_Input.Value;
   begin
      if Key /= "" then
         View.Error_Label.Text ("No error.");
         Localize.Parser.Insert (View.Locale, Key);
         On_Select (Object);
      else
         View.Error_Label.Text ("Empty key.");
      end if;
   exception
      when others =>
         View.Error_Label.Text ("Error with key: " & Key);
   end On_Insert;

   procedure On_Delete (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      Key : constant String := View.Key_Input.Value;
   begin
      if Key /= "" then
         View.Error_Label.Text ("No error.");
         Localize.Parser.Delete (View.Locale, Key);
         On_Select (Object);
      else
         View.Error_Label.Text ("Empty key.");
      end if;
   exception
      when others =>
         View.Error_Label.Text ("Error with key: " & Key);
   end On_Delete;

   procedure On_Rename (Object : in out Gnoga.Gui.Base.Base_Type'Class) is
      View : constant Localize.View.Default_View_Access :=
        Localize.View.Default_View_Access (Object.Parent.Parent.Parent);
      From : constant String := View.Key_List.Value;
      To   : constant String := View.Key_Input.Value;
   begin
      if From /= "" and To /= "" then
         View.Error_Label.Text ("No error.");
         Localize.Parser.Rename (View.Locale, From, To);
         On_Select (Object);
      else
         View.Error_Label.Text ("Empty key.");
      end if;
   exception
      when others =>
         View.Error_Label.Text ("Error with keys: " & From & " and " & To);
   end On_Rename;

   procedure Default
     (Main_Window : in out Gnoga.Gui.Window.Window_Type'Class;
      Connection  :        access Gnoga.Application.Multi_Connect.Connection_Holder_Type)
   is
      pragma Unreferenced (Connection);
      View : constant Localize.View.Default_View_Access := new Localize.View.Default_View_Type;
   begin
      View.Dynamic;
      View.Main_Window   := Main_Window'Unchecked_Access;
      View.Old_Key_Index := 0;
      View.Create (Main_Window, Gnoga.Gui.View.Grid.Horizontal_Split);
      --  Avoid Enter to bubble up a new connection
      View.On_Submit_Handler (On_Enter'Access);
      View.Load_Button.On_Click_Handler (On_Load'Access);
      View.Key_List.On_Change_Handler (On_Change_Key'Access);
      View.Key_List.On_Click_Handler (On_Change_Key'Access);
      View.Save_Button.On_Click_Handler (On_Save'Access);
      View.Exit_Button.On_Click_Handler (On_Exit'Access);
      View.Quit_Button.On_Click_Handler (On_Quit'Access);
      View.Select_Pattern.On_Change_Handler (On_Select'Access);
      View.Duplicate_Button.On_Click_Handler (On_Duplicate'Access);
      View.Insert_Button.On_Click_Handler (On_Insert'Access);
      View.Delete_Button.On_Click_Handler (On_Delete'Access);
      View.Rename_Button.On_Click_Handler (On_Rename'Access);
      View.Locale_Text.On_Change_Handler (On_Change_Text'Access);
      View.Locale_Comment.On_Change_Handler (On_Change_Comment'Access);
   end Default;

begin
   Gnoga.Application.Multi_Connect.On_Connect_Handler (Default'Access, "default");
end Localize.Controller;

localize-parser.ads

-------------------------------------------------------------------------------
-- NAME (specification)         : localize-parser.ads
-- AUTHOR                       : Pascal Pignard
-- ROLE                         : Localization files parser unit.
-- NOTES                        : Ada 2012, GNOGA 2.1 alpha
--
-- COPYRIGHT                    : (c) Pascal Pignard 2021
-- LICENCE                      : CeCILL V2 (http://www.cecill.info)
-- CONTACT                      : http://blady.pagesperso-orange.fr
-------------------------------------------------------------------------------

with Ada.Containers.Indefinite_Vectors;
with Ada.Containers.Ordered_Maps;

package Localize.Parser is

   package Lists is new Ada.Containers.Indefinite_Vectors (Positive, String);
   subtype Key_List is Lists.Vector;

   type Property_List is private;

   procedure Read
     (Properties : out Property_List;
      File_Name  :     String);
   procedure Write
     (Properties : Property_List;
      File_Name  : String);

   function Keys
     (Properties : Property_List)
      return Key_List;
   function Selected_Keys
     (Master, Locale : Property_List;
      Pattern        : String)
      return Key_List;
   function Contains
     (Properties : Property_List;
      Key        : String)
      return Boolean;

   function Text
     (Properties : Property_List;
      Key        : String)
      return String;
   procedure Text
     (Properties : in out Property_List;
      Key        :        String;
      Value      :        String);
   function Comment
     (Properties : Property_List;
      Key        : String)
      return String;
   procedure Comment
     (Properties : in out Property_List;
      Key        :        String;
      Value      :        String);

   procedure Insert
     (Properties : in out Property_List;
      Key        :        String);
   procedure Delete
     (Properties : in out Property_List;
      Key        :        String);
   procedure Rename
     (Properties : in out Property_List;
      From, To   :        String);
   function Modified
     (Properties : Property_List;
      Key        : String)
      return Boolean;
   procedure Reset_Modified_Indicators (Properties : in out Property_List);

private

   type Property_Type is record
      Comment  : String;
      Text     : String;
      Modified : Boolean;
   end record;
   package Content_Maps is new Ada.Containers.Ordered_Maps (String, Property_Type);
   type Property_List is new Content_Maps.Map with null record;

end Localize.Parser;

localize-parser.adb

-------------------------------------------------------------------------------
-- NAME (body)                  : localize-parser.adb
-- AUTHOR                       : Pascal Pignard
-- ROLE                         : Localization files parser unit.
-- NOTES                        : Ada 2012, GNOGA 2.1 alpha
--
-- COPYRIGHT                    : (c) Pascal Pignard 2021
-- LICENCE                      : CeCILL V2 (http://www.cecill.info)
-- CONTACT                      : http://blady.pagesperso-orange.fr
-------------------------------------------------------------------------------

with Ada.Characters.Wide_Wide_Latin_1;
with Gnoga;
with UXStrings.Formatting;
with UXStrings.Text_IO;

package body Localize.Parser is

   use type Content_Maps.Cursor;

   procedure Parse_Strings_File
     (File_Name :     String;
      Content   : out Property_List)
   is
      type State_Type is (None, In_Comment, In_Key, In_Value, Equal, Semi_Colon);

      Raw_File : UXStrings.Text_IO.File_Type;
      Text     : String;
      C        : Unicode_Character;
      I        : Natural;
      State    : State_Type := None;
      Comment  : String;
      Key      : String;
      Value    : String;

      procedure Append (C : Unicode_Character) is
      begin
         case State is
            when In_Comment =>
               Append (Comment, C);
            when In_Key =>
               Append (Key, C);
            when In_Value =>
               Append (Value, C);
            when others =>
               null;
         end case;
      end Append;

   begin
      UXStrings.Text_IO.Open (Raw_File, UXStrings.Text_IO.In_File, File_Name, UTF_16LE, UXStrings.Text_IO.LF_Ending);
      while not UXStrings.Text_IO.End_Of_File (Raw_File) loop
         UXStrings.Text_IO.Get (Raw_File, C);
         Text.Append (C);
      end loop;
      UXStrings.Text_IO.Close (Raw_File);

      Content.Clear;
      I := Text.First;
      while I <= Text.Last loop
         if Text (I) = '\' then
            Text.Next (I);
            case Text (I) is
               when 't' =>
                  Append (Ada.Characters.Wide_Wide_Latin_1.HT);
               when 'n' =>
                  Append (Ada.Characters.Wide_Wide_Latin_1.LF);
               when Ada.Characters.Wide_Wide_Latin_1.LF =>
                  null;
               when '"' | '\' =>
                  Append (Text (I));
               when others =>
                  Append ('\');
                  Append (Text (I));
            end case;
         elsif Text (I) = '/' and State = None then
            Text.Next (I);
            if Text (I) = '*' then
               State := In_Comment;
            end if;
         elsif Text (I) /= '*' and State = In_Comment then
            Append (Comment, Text (I));
         elsif Text (I) = '*' and State = In_Comment then
            Text.Next (I);
            if Text (I) = '/' then
               State := None;
            else
               Append (Comment, '*');
               Append (Comment, Text (I));
            end if;
         elsif Text (I) = '"' and State = None then
            State := In_Key;
         elsif Text (I) /= '"' and State = In_Key then
            Append (Key, Text (I));
         elsif Text (I) = '"' and State = In_Key then
            State := Equal;
         elsif Text (I) = '"' and State = Equal then
            State := In_Value;
         elsif Text (I) /= '"' and State = In_Value then
            Append (Value, Text (I));
         elsif Text (I) = '"' and State = In_Value then
            State := Semi_Colon;
         elsif Text (I) = ';' and State = Semi_Colon then
            Content.Insert (Key, (Comment, Value, False));
            Comment := Null_UXString;
            Key     := Null_UXString;
            Value   := Null_UXString;
            State   := None;
         end if;
         Text.Next (I);
      end loop;
   end Parse_Strings_File;

   procedure Write_Strings_File
     (File_Name : String;
      Content   : Property_List)
   is
      Raw_File : UXStrings.Text_IO.File_Type;

      procedure Escaped_Put
        (Str           : String;
         Multi_Comment : Boolean := False)
      is
      begin
         for I in 1 .. Length (Str) loop
            if Element (Str, I) = Ada.Characters.Wide_Wide_Latin_1.HT then
               UXStrings.Text_IO.Put (Raw_File, '\');
               UXStrings.Text_IO.Put (Raw_File, 't');
            elsif Element (Str, I) = Ada.Characters.Wide_Wide_Latin_1.LF then
               if Multi_Comment then
                  UXStrings.Text_IO.New_Line (Raw_File);
               else
                  UXStrings.Text_IO.Put (Raw_File, '\');
                  UXStrings.Text_IO.Put (Raw_File, 'n');
               end if;
            elsif Element (Str, I) in '"' | '\' then
               UXStrings.Text_IO.Put (Raw_File, '\');
               UXStrings.Text_IO.Put (Raw_File, Element (Str, I));
            else
               UXStrings.Text_IO.Put (Raw_File, Element (Str, I));
            end if;
         end loop;
      end Escaped_Put;

   begin
      UXStrings.Text_IO.Create (Raw_File, UXStrings.Text_IO.Out_File, File_Name, UTF_16LE, UXStrings.Text_IO.LF_Ending);
      UXStrings.Text_IO.Put_BOM (Raw_File);
      for C in Content.Iterate loop
         if Content_Maps.Element (C).Comment /= Null_UXString then
            UXStrings.Text_IO.Put (Raw_File, '/');
            UXStrings.Text_IO.Put (Raw_File, '*');
            Escaped_Put (Content_Maps.Element (C).Comment, True);
            UXStrings.Text_IO.Put (Raw_File, '*');
            UXStrings.Text_IO.Put (Raw_File, '/');
            UXStrings.Text_IO.New_Line (Raw_File);
         end if;
         UXStrings.Text_IO.Put (Raw_File, '"');
         Escaped_Put (Content_Maps.Key (C));
         UXStrings.Text_IO.Put (Raw_File, '"');
         UXStrings.Text_IO.Put (Raw_File, ' ');
         UXStrings.Text_IO.Put (Raw_File, '=');
         UXStrings.Text_IO.Put (Raw_File, ' ');
         UXStrings.Text_IO.Put (Raw_File, '"');
         Escaped_Put (Content_Maps.Element (C).Text);
         UXStrings.Text_IO.Put (Raw_File, '"');
         UXStrings.Text_IO.Put (Raw_File, ';');
         UXStrings.Text_IO.New_Line (Raw_File);
      end loop;
      UXStrings.Text_IO.Close (Raw_File);
   end Write_Strings_File;

   procedure Parse_Properties_File
     (File_Name :     String;
      Content   : out Property_List)
   is
      type State_Type is (None, In_Comment, In_Key, In_Value, Equal);

      Raw_File : UXStrings.Text_IO.File_Type;
      Text     : String;
      C        : Unicode_Character;
      I        : Natural;
      State    : State_Type := None;
      Comment  : String;
      Key      : String;
      Value    : String;
      Hex4     : String     := "0000";

      procedure Append (C : Wide_Wide_Character) is
      begin
         case State is
            when In_Comment =>
               Append (Comment, C);
            when In_Key =>
               Append (Key, C);
            when In_Value =>
               Append (Value, C);
            when None =>
               State := In_Key;
               Append (Key, C);
            when Equal =>
               State := In_Value;
               Append (Value, C);
         end case;
      end Append;

   begin
      UXStrings.Text_IO.Open (Raw_File, UXStrings.Text_IO.In_File, File_Name, Latin_1, UXStrings.Text_IO.LF_Ending);
      while not UXStrings.Text_IO.End_Of_File (Raw_File) loop
         UXStrings.Text_IO.Get (Raw_File, C);
         Text.Append (C);
      end loop;
      UXStrings.Text_IO.Close (Raw_File);

      Content.Clear;
      I := Text.First;
      while I <= Text.Last loop
         if Text (I) = '\' then
            Text.Next (I);
            case Text (I) is
               when 'u' =>
                  for J in Hex4 loop
                     Text.Next (I);
                     exit when I > Text.Last;
                     Hex4.Replace_Unicode (J, Text (I));
                  end loop;
                  Append (Wide_Wide_Character'Val (Gnoga.Value (Hex4, 16)));
               when ' ' | ':' | '=' | '#' | '!' | '\' =>
                  Append (Text (I));
               when 't' =>
                  Append (Ada.Characters.Wide_Wide_Latin_1.HT);
               when 'n' =>
                  Append (Ada.Characters.Wide_Wide_Latin_1.LF);
               when Ada.Characters.Wide_Wide_Latin_1.LF =>
                  null;
               when others =>
                  Append ('\');
                  Append (Text (I));
            end case;
         elsif Text (I) = Ada.Characters.Wide_Wide_Latin_1.LF and State = In_Comment then
            State := None;
         elsif State = In_Comment then
            Append (Comment, Text (I));
         elsif Text (I) = Ada.Characters.Wide_Wide_Latin_1.LF and State in In_Value | Equal | In_Key then
            Content.Include (Key, (Comment, Value, False));
            Comment := Null_UXString;
            Key     := Null_UXString;
            Value   := Null_UXString;
            State   := None;
         elsif Text (I) in '#' | '!' and State = None then
            State := In_Comment;
            if Comment /= Null_UXString then
               Append (Ada.Characters.Wide_Wide_Latin_1.LF);
            end if;
         elsif Text (I) in '=' | ':' and State = In_Key then
            State := Equal;
         elsif Text (I) not in Ada.Characters.Wide_Wide_Latin_1.HT | Ada.Characters.Wide_Wide_Latin_1.LF |
               Ada.Characters.Wide_Wide_Latin_1.FF | ' ' and
           State in In_Key
         then
            Append (Key, (Text (I)));
         elsif Text (I) not in Ada.Characters.Wide_Wide_Latin_1.HT | Ada.Characters.Wide_Wide_Latin_1.LF |
               Ada.Characters.Wide_Wide_Latin_1.FF | ' ' and
           State in None
         then
            State := In_Key;
            Append (Key, Text (I));
         elsif Text (I) not in Ada.Characters.Wide_Wide_Latin_1.HT | Ada.Characters.Wide_Wide_Latin_1.FF and
           State in In_Value
         then
            Append (Value, Text (I));
         elsif Text (I) not in Ada.Characters.Wide_Wide_Latin_1.HT | Ada.Characters.Wide_Wide_Latin_1.FF | ' ' and
           State in Equal
         then
            State := In_Value;
            Append (Value, Text (I));
         end if;
         Text.Next (I);
      end loop;
   end Parse_Properties_File;

   procedure Write_Properties_File
     (File_Name : String;
      Content   : Property_List)
   is
      Raw_File : UXStrings.Text_IO.File_Type;

      type Escape_Space_Type is (No, Start, Full);

      procedure Escaped_Write
        (Str           : String;
         Escape_Space  : Escape_Space_Type;
         Multi_Comment : Boolean := False)
      is
         Space : Boolean := Escape_Space = Start;
         use UXStrings.Formatting;
         use all type UXStrings.Formatting.Alignment;
         function Format is new Integer_Format (Natural);
         function Format_U16
           (Item    :    Natural;
            Base    : in Number_Base := 16;
            PutPlus : in Boolean     := False;
            Field   : in Natural     := 4;
            Justify : in Alignment   := Right;
            Fill    : in Character   := '0')
            return UXString renames Format;
      begin
         for I in 1 .. Length (Str) loop
            if Element (Str, I) = Ada.Characters.Wide_Wide_Latin_1.HT then
               UXStrings.Text_IO.Put (Raw_File, '\');
               UXStrings.Text_IO.Put (Raw_File, 't');
               Space := False;
            elsif Element (Str, I) = Ada.Characters.Wide_Wide_Latin_1.LF then
               if Multi_Comment then
                  UXStrings.Text_IO.New_Line (Raw_File);
                  UXStrings.Text_IO.Put (Raw_File, '#');
               else
                  UXStrings.Text_IO.Put (Raw_File, '\');
                  UXStrings.Text_IO.Put (Raw_File, 'n');
               end if;
               Space := False;
            elsif Element (Str, I) in '=' | ':' | '#' | '!' | '\' and Escape_Space /= No then
               UXStrings.Text_IO.Put (Raw_File, '\');
               UXStrings.Text_IO.Put (Raw_File, Element (Str, I));
               Space := False;
            elsif Element (Str, I) = ' ' and (Space or Escape_Space = Full) then
               UXStrings.Text_IO.Put (Raw_File, '\');
               UXStrings.Text_IO.Put (Raw_File, ' ');
               Space := False;
            elsif Element (Str, I) < Ada.Characters.Wide_Wide_Latin_1.DEL then
               UXStrings.Text_IO.Put (Raw_File, Element (Str, I));
               Space := False;
            else
               UXStrings.Text_IO.Put (Raw_File, '\');
               UXStrings.Text_IO.Put (Raw_File, 'u');
               declare
                  Hex4 : constant String := Format_U16 (BMP_Character'Pos (Get_BMP (Str, I, '?')));
               begin
                  for C of Hex4 loop
                     UXStrings.Text_IO.Put (Raw_File, C);
                  end loop;
               end;
               Space := False;
            end if;
         end loop;
      end Escaped_Write;

   begin
      UXStrings.Text_IO.Create (Raw_File, UXStrings.Text_IO.Out_File, File_Name, Latin_1, UXStrings.Text_IO.LF_Ending);
      for C in Content.Iterate loop
         if Content_Maps.Element (C).Comment /= Null_UXString then
            UXStrings.Text_IO.Put (Raw_File, '#');
            Escaped_Write (Content_Maps.Element (C).Comment, No, True);
            UXStrings.Text_IO.New_Line (Raw_File);
         end if;
         Escaped_Write (Content_Maps.Key (C), Full);
         UXStrings.Text_IO.Put (Raw_File, '=');
         Escaped_Write (Content_Maps.Element (C).Text, Start);
         UXStrings.Text_IO.New_Line (Raw_File);
      end loop;
      UXStrings.Text_IO.Close (Raw_File);
   end Write_Properties_File;

   procedure Read
     (Properties : out Property_List;
      File_Name  :     String)
   is
   begin
      if Tail (File_Name, 8, ' ') = ".strings" then
         Parse_Strings_File (File_Name, Properties);
      end if;
      if Tail (File_Name, 11, ' ') = ".properties" then
         Parse_Properties_File (File_Name, Properties);
      end if;
   end Read;

   procedure Write
     (Properties : Property_List;
      File_Name  : String)
   is
   begin
      if Tail (File_Name, 8, ' ') = ".strings" then
         Write_Strings_File (File_Name, Properties);
      end if;
      if Tail (File_Name, 11, ' ') = ".properties" then
         Write_Properties_File (File_Name, Properties);
      end if;
   end Write;

   function Keys
     (Properties : Property_List)
      return Key_List
   is
   begin
      return Result_Key_List : Key_List do
         for Index in Properties.Iterate loop
            Result_Key_List.Append (Content_Maps.Key (Index));
         end loop;
      end return;
   end Keys;

   function Contains
     (Properties : Property_List;
      Key        : String)
      return Boolean is (Content_Maps.Contains (Content_Maps.Map (Properties), Key));

   procedure Insert
     (Properties : in out Property_List;
      Key        :        String)
   is
   begin
      Properties.Insert (Key, (Null_UXString, Null_UXString, False));
   end Insert;

   procedure Delete
     (Properties : in out Property_List;
      Key        :        String)
   is
   begin
      if Properties.Find (Key) /= Content_Maps.No_Element then
         Properties.Delete (Key);
      end if;
   end Delete;

   procedure Rename
     (Properties : in out Property_List;
      From, To   :        String)
   is
   begin
      Properties.Insert (To, Properties.Element (From));
      Properties.Delete (From);
   end Rename;

   function Text
     (Properties : Property_List;
      Key        : String)
      return String
   is
   begin
      if Properties.Find (Key) = Content_Maps.No_Element then
         return "";
      else
         return Properties.Element (Key).Text;
      end if;
   end Text;

   procedure Text
     (Properties : in out Property_List;
      Key        :        String;
      Value      :        String)
   is
      Cursor  : constant Content_Maps.Cursor := Properties.Find (Key);
      Element : Property_Type;
   begin
      if Cursor /= Content_Maps.No_Element then
         Element      := Content_Maps.Element (Cursor);
         Element.Text := Value;
         if Element.Text /= Content_Maps.Element (Cursor).Text then
            Element.Modified := True;
            Properties.Replace_Element (Cursor, Element);
         end if;
      end if;
   end Text;

   function Comment
     (Properties : Property_List;
      Key        : String)
      return String
   is
   begin
      if Properties.Find (Key) = Content_Maps.No_Element then
         return Null_UXString;
      else
         return Properties.Element (Key).Comment;
      end if;
   end Comment;

   procedure Comment
     (Properties : in out Property_List;
      Key        :        String;
      Value      :        String)
   is
      Cursor  : constant Content_Maps.Cursor := Properties.Find (Key);
      Element : Property_Type;
   begin
      if Cursor /= Content_Maps.No_Element then
         Element         := Content_Maps.Element (Cursor);
         Element.Comment := Value;
         if Element.Comment /= Content_Maps.Element (Cursor).Comment then
            Element.Modified := True;
            Properties.Replace_Element (Cursor, Element);
         end if;
      end if;
   end Comment;

   function Modified
     (Properties : Property_List;
      Key        : String)
      return Boolean
   is
   begin
      if Properties.Find (Key) = Content_Maps.No_Element then
         return False;
      else
         return Properties.Element (Key).Modified;
      end if;
   end Modified;

   procedure Reset_Modified_Indicators (Properties : in out Property_List) is
   begin
      for Element of Properties loop
         Element.Modified := False;
      end loop;
   end Reset_Modified_Indicators;

   function Selected_Keys
     (Master, Locale : Property_List;
      Pattern        : String)
      return Key_List
   is
      package Keys_Sorting is new Lists.Generic_Sorting;
   begin
      if Pattern /= "" then
         return Result_Key_List : Key_List do
            for Cursor in Master.Iterate loop
               if
                 (Index (Content_Maps.Key (Cursor), Pattern) > 0 or
                  Index (Content_Maps.Element (Cursor).Text, Pattern) > 0 or
                  Index (Content_Maps.Element (Cursor).Comment, Pattern) > 0) and
                 not Result_Key_List.Contains (Content_Maps.Key (Cursor))
               then
                  Result_Key_List.Append (Content_Maps.Key (Cursor));
               end if;
            end loop;
            for Cursor in Locale.Iterate loop
               if
                 (Index (Content_Maps.Key (Cursor), Pattern) > 0 or
                  Index (Content_Maps.Element (Cursor).Text, Pattern) > 0 or
                  Index (Content_Maps.Element (Cursor).Comment, Pattern) > 0) and
                 not Result_Key_List.Contains (Content_Maps.Key (Cursor))
               then
                  Result_Key_List.Append (Content_Maps.Key (Cursor));
               end if;
            end loop;
         end return;
      else
         return Result_Key_List : Key_List := Keys (Master) do
            for Cursor in Locale.Iterate loop
               if not Result_Key_List.Contains (Content_Maps.Key (Cursor)) then
                  Result_Key_List.Append (Content_Maps.Key (Cursor));
               end if;
            end loop;
            Keys_Sorting.Sort (Result_Key_List);
         end return;
      end if;
   end Selected_Keys;

end Localize.Parser;

localize-main.adb

-------------------------------------------------------------------------------
-- NAME (body)                  : localize-main.adb
-- AUTHOR                       : Pascal Pignard
-- ROLE                         : Main unit.
-- NOTES                        : Ada 2012, GNOGA 2.1 alpha
--
-- COPYRIGHT                    : (c) Pascal Pignard 2021
-- LICENCE                      : CeCILL V2 (http://www.cecill.info)
-- CONTACT                      : http://blady.pagesperso-orange.fr
-------------------------------------------------------------------------------

with Gnoga.Application.Multi_Connect;

--  Needed to register connection in body initialization
with Localize.Controller;
pragma Unreferenced (Localize.Controller);

procedure Localize.Main is
begin
   Gnoga.Application.Title (Name => "Localize (V1.1-alpha)");
   Gnoga.Application.Multi_Connect.Initialize;
   Gnoga.Application.Multi_Connect.Message_Loop;
exception
   when E : others =>
      Gnoga.Log (E);
end Localize.Main;

Project file

with "settings.gpr";
with "gnoga.gpr";

project Localize is

   for Object_Dir use Settings.Obj_Dir;
   for Exec_Dir use Settings.Exe_Dir;
   for Main use ("localize-main.adb");
   for Create_Missing_Dirs use Settings'Create_Missing_Dirs;

   package Builder is
      for Executable ("localize-main.adb") use "localize";
   end Builder;

   package Compiler is
      for Default_Switches ("Ada") use Settings.Compiler'Default_Switches ("Ada") & "-gnatyN";
   end Compiler;

   package Binder renames Settings.Binder;
   package Linker renames Settings.Linker;
   package Pretty_Printer renames Settings.Pretty_Printer;

end Localize;