Repository: WeblateOrg/weblate

Issue:
Support for INI files
It seems to be quite popular, so Weblate should support it.

See also discussion at https://github.com/gogits/gogs/issues/571
## 


<bountysource-plugin>

---
Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/6794135-support-for-ini-files?utm_campaign=plugin&utm_content=tracker%2F253393&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F253393&utm_medium=issues&utm_source=github).
</bountysource-plugin>


Discussion:
First the inifile implementation in translate-toolkit would need some improvements to behave a bit more  standard (eg. findid will work there).

Also there is existing support for Joomla INI based translations.
Note that translate-toolkit INI files works only on Python 2 right now, what is also a blocker for using that. See also https://github.com/translate/translate/issues/3348

Resolving patch:
diff --git a/weblate/formats/models.py b/weblate/formats/models.py
--- a/weblate/formats/models.py
+++ b/weblate/formats/models.py
@@ -88,6 +88,8 @@ class FormatsConf(AppConf):
         "weblate.formats.ttkit.SubStationAlphaFormat",
         "weblate.formats.ttkit.DTDFormat",
         "weblate.formats.ttkit.FlatXMLFormat",
+        "weblate.formats.ttkit.INIFormat",
+        "weblate.formats.ttkit.InnoSetupINIFormat",
         "weblate.formats.external.XlsxFormat",
         "weblate.formats.txt.AppStoreFormat",
         "weblate.formats.convert.HTMLFormat",
diff --git a/weblate/formats/ttkit.py b/weblate/formats/ttkit.py
--- a/weblate/formats/ttkit.py
+++ b/weblate/formats/ttkit.py
@@ -256,10 +256,14 @@ def get_class(cls):
         # Get the class
         return getattr(module, class_name)
 
+    @staticmethod
+    def get_class_kwargs():
+        return {}
+
     @classmethod
     def parse_store(cls, storefile):
         """Parse the store."""
-        store = cls.get_class()()
+        store = cls.get_class()(**cls.get_class_kwargs())
 
         # Apply possible fixups
         cls.fixup(store)
@@ -731,6 +735,24 @@ def target(self):
         return self.unit.source
 
 
+class INIUnit(TTKitUnit):
+    @cached_property
+    def locations(self):
+        return ""
+
+    @cached_property
+    def context(self):
+        if self.template is not None:
+            return self.template.location
+        return self.unit.location
+
+    def has_content(self):
+        return True
+
+    def is_readonly(self):
+        return False
+
+
 class BasePoFormat(TTKitFormat, BilingualUpdateMixin):
     loader = pofile
 
@@ -1280,3 +1302,54 @@ class FlatXMLFormat(TTKitFormat):
     monolingual = True
     unit_class = FlatXMLUnit
     new_translation = '<?xml version="1.0" encoding="utf-8"?>\n<root></root>'
+
+
+class INIFormat(TTKitFormat):
+    name = _("INI file")
+    format_id = "ini"
+    loader = ("ini", "inifile")
+    monolingual = True
+    unit_class = INIUnit
+    new_translation = "\n"
+
+    @staticmethod
+    def mimetype():
+        """Return most common media type for format."""
+        # INI files do not expose mimetype
+        return "text/plain"
+
+    @classmethod
+    def extension(cls):
+        """Return most common file extension for format."""
+        # INI files do not expose extension
+        return "ini"
+
+    @classmethod
+    def load(cls, storefile):
+        store = super().load(storefile)
+        # Adjust store to have translations
+        for unit in store.units:
+            unit.target = unit.source
+            unit.rich_target = unit.rich_source
+        return store
+
+    def create_unit(self, key, source):
+        unit = super().create_unit(key, source)
+        unit.location = key
+        return unit
+
+
+class InnoSetupINIFormat(INIFormat):
+    name = _("InnoSetup INI file")
+    format_id = "islu"
+    loader = ("ini", "inifile")
+
+    @classmethod
+    def extension(cls):
+        """Return most common file extension for format."""
+        # INI files do not expose extension
+        return "islu"
+
+    @staticmethod
+    def get_class_kwargs():
+        return {"dialect": "inno"}


Test patch:
diff --git a/weblate/formats/tests/test_formats.py b/weblate/formats/tests/test_formats.py
--- a/weblate/formats/tests/test_formats.py
+++ b/weblate/formats/tests/test_formats.py
@@ -36,6 +36,8 @@
     CSVSimpleFormat,
     DTDFormat,
     FlatXMLFormat,
+    INIFormat,
+    InnoSetupINIFormat,
     JoomlaFormat,
     JSONFormat,
     JSONNestedFormat,
@@ -63,7 +65,8 @@
 TEST_NESTED_JSON = get_test_file("cs-nested.json")
 TEST_WEBEXT_JSON = get_test_file("cs-webext.json")
 TEST_PHP = get_test_file("cs.php")
-TEST_JOOMLA = get_test_file("cs.ini")
+TEST_JOOMLA = get_test_file("cs.joomla.ini")
+TEST_INI = get_test_file("cs.ini")
 TEST_PROPERTIES = get_test_file("swing.properties")
 TEST_ANDROID = get_test_file("strings.xml")
 TEST_XLIFF = get_test_file("cs.xliff")
@@ -158,6 +161,7 @@ class AutoFormatTest(FixtureTestCase, TempDirMixin):
     FIND_CONTEXT = ""
     FIND_MATCH = "Ahoj světe!\n"
     NEW_UNIT_MATCH = b'\nmsgid "key"\nmsgstr "Source string"\n'
+    NEW_UNIT_KEY = "key"
     SUPPORTS_FLAG = True
     EXPECTED_FLAGS = "c-format, max-length:100"
     EDIT_OFFSET = 0
@@ -273,7 +277,7 @@ def test_new_unit(self):
         storage = self.parse_file(testfile)
 
         # Add new unit
-        storage.new_unit("key", "Source string")
+        storage.new_unit(self.NEW_UNIT_KEY, "Source string")
 
         # Read new content
         with open(testfile, "rb") as handle:
@@ -761,3 +765,26 @@ class FlatXMLFormatTest(AutoFormatTest):
     FIND_MATCH = "Hello World!"
     NEW_UNIT_MATCH = b'<str key="key">Source string</str>\n'
     EXPECTED_FLAGS = ""
+
+
+class INIFormatTest(AutoFormatTest):
+    FORMAT = INIFormat
+    FILE = TEST_INI
+    MIME = "text/plain"
+    COUNT = 4
+    BASE = ""
+    EXT = "ini"
+    MASK = "ini/*.ini"
+    EXPECTED_PATH = "ini/cs_CZ.ini"
+    MATCH = "\n"
+    FIND = 'Ahoj "světe"!\\n'
+    FIND_CONTEXT = "[weblate]hello"
+    FIND_MATCH = 'Ahoj "světe"!\\n'
+    NEW_UNIT_MATCH = b"\nkey = Source string"
+    NEW_UNIT_KEY = "[test]key"
+    EXPECTED_FLAGS = ""
+
+
+class InnoSetupINIFormatTest(INIFormatTest):
+    FORMAT = InnoSetupINIFormat
+    EXT = "islu"
diff --git a/weblate/trans/tests/data/cs.ini b/weblate/trans/tests/data/cs.ini
--- a/weblate/trans/tests/data/cs.ini
+++ b/weblate/trans/tests/data/cs.ini
@@ -1,4 +1,5 @@
-HELLO="Ahoj "_QQ_"světe"_QQ_"!\n"
-ORANGUTAN="Orangutan má %d banán.\n"
-TRY="Zkus Weblate na <https://demo.weblate.org/>!\n"
-THANKS="Děkujeme za použití Weblate."
+[weblate]
+hello=Ahoj "světe"!\n
+orangutan=Orangutan má %d banán.\n
+try=Zkus Weblate na <https://demo.weblate.org/>!\n
+thanks=Děkujeme za použití Weblate.
diff --git a/weblate/trans/tests/data/cs.joomla.ini b/weblate/trans/tests/data/cs.joomla.ini
new file mode 100644
--- /dev/null
+++ b/weblate/trans/tests/data/cs.joomla.ini
@@ -0,0 +1,4 @@
+HELLO="Ahoj "_QQ_"světe"_QQ_"!\n"
+ORANGUTAN="Orangutan má %d banán.\n"
+TRY="Zkus Weblate na <https://demo.weblate.org/>!\n"
+THANKS="Děkujeme za použití Weblate."
diff --git a/weblate/trans/tests/test_component.py b/weblate/trans/tests/test_component.py
--- a/weblate/trans/tests/test_component.py
+++ b/weblate/trans/tests/test_component.py
@@ -61,7 +61,12 @@ def verify_component(
             self.assertEqual(translation.unit_set.count(), units)
             # Check whether unit exists
             if units:
-                self.assertTrue(translation.unit_set.filter(source=unit).exists())
+                self.assertTrue(
+                    translation.unit_set.filter(source=unit).exists(),
+                    "Unit not found, all units: {}".format(
+                        "\n".join(translation.unit_set.values_list("source", flat=True))
+                    ),
+                )
 
         if component.has_template() and component.edit_template:
             translation = component.translation_set.get(filename=component.template)
@@ -255,6 +260,10 @@ def test_create_joomla(self):
         component = self.create_joomla()
         self.verify_component(component, 3, "cs", 4)
 
+    def test_create_ini(self):
+        component = self.create_ini()
+        self.verify_component(component, 2, "cs", 4, "Hello, world!\\n")
+
     def test_create_tsv_simple(self):
         component = self._create_component("csv-simple", "tsv/*.txt")
         self.verify_component(component, 2, "cs", 4, "Hello, world!")
diff --git a/weblate/trans/tests/utils.py b/weblate/trans/tests/utils.py
--- a/weblate/trans/tests/utils.py
+++ b/weblate/trans/tests/utils.py
@@ -327,6 +327,9 @@ def create_json_intermediate_empty(self, **kwargs):
     def create_joomla(self):
         return self._create_component("joomla", "joomla/*.ini", "joomla/en-GB.ini")
 
+    def create_ini(self):
+        return self._create_component("ini", "ini/*.ini", "ini/en.ini")
+
     def create_tsv(self):
         return self._create_component("csv", "tsv/*.txt")