1from io import BytesIO
2
3from translate.convert import prop2po, test_convert
4from translate.storage import po, properties
5
6
7class TestProp2PO:
8    def prop2po(self, propsource, proptemplate=None, personality="java"):
9        """helper that converts .properties source to po source without requiring files"""
10        inputfile = BytesIO(propsource.encode())
11        inputprop = properties.propfile(inputfile, personality=personality)
12        convertor = prop2po.prop2po(personality=personality)
13        if proptemplate:
14            templatefile = BytesIO(proptemplate.encode())
15            templateprop = properties.propfile(templatefile)
16            outputpo = convertor.mergestore(templateprop, inputprop)
17        else:
18            outputpo = convertor.convertstore(inputprop)
19        return outputpo
20
21    def convertprop(self, propsource):
22        """call the convertprop, return the outputfile"""
23        inputfile = BytesIO(propsource.encode())
24        outputfile = BytesIO()
25        templatefile = None
26        assert prop2po.convertprop(inputfile, outputfile, templatefile)
27        return outputfile.getvalue()
28
29    def singleelement(self, pofile):
30        """checks that the pofile contains a single non-header element, and returns it"""
31        assert len(pofile.units) == 2
32        assert pofile.units[0].isheader()
33        print(pofile)
34        return pofile.units[1]
35
36    def countelements(self, pofile):
37        """counts the number of non-header entries"""
38        assert pofile.units[0].isheader()
39        print(pofile)
40        return len(pofile.units) - 1
41
42    def test_simpleentry(self):
43        """checks that a simple properties entry converts properly to a po entry"""
44        propsource = "SAVEENTRY=Save file\n"
45        pofile = self.prop2po(propsource)
46        pounit = self.singleelement(pofile)
47        assert pounit.source == "Save file"
48        assert pounit.target == ""
49
50    def test_convertprop(self):
51        """checks that the convertprop function is working"""
52        propsource = "SAVEENTRY=Save file\n"
53        posource = self.convertprop(propsource)
54        pofile = po.pofile(BytesIO(posource))
55        pounit = self.singleelement(pofile)
56        assert pounit.source == "Save file"
57        assert pounit.target == ""
58
59    def test_no_value_entry(self):
60        """checks that a properties entry without value is converted"""
61        propsource = "KEY = \n"
62        pofile = self.prop2po(propsource)
63        pounit = self.singleelement(pofile)
64        assert pounit.getcontext() == "KEY"
65        assert pounit.source == ""
66        assert pounit.target == ""
67
68    def test_no_separator_entry(self):
69        """checks that a properties entry without separator is converted"""
70        propsource = "KEY\n"
71        pofile = self.prop2po(propsource)
72        pounit = self.singleelement(pofile)
73        assert pounit.getcontext() == "KEY"
74        assert pounit.source == ""
75        assert pounit.target == ""
76
77    def test_tab_at_end_of_string(self):
78        """check that we preserve tabs at the end of a string"""
79        propsource = r"TAB_AT_END=This setence has a tab at the end.\t"
80        pofile = self.prop2po(propsource)
81        pounit = self.singleelement(pofile)
82        assert pounit.source == "This setence has a tab at the end.\t"
83
84        propsource = (
85            r"SPACE_THEN_TAB_AT_END=This setence has a space then tab at the end. \t"
86        )
87        pofile = self.prop2po(propsource)
88        pounit = self.singleelement(pofile)
89        assert pounit.source == "This setence has a space then tab at the end. \t"
90
91        propsource = r"SPACE_AT_END=This setence will keep its 4 spaces at the end.    "
92        pofile = self.prop2po(propsource)
93        pounit = self.singleelement(pofile)
94        assert pounit.source == "This setence will keep its 4 spaces at the end.    "
95
96        propsource = (
97            r"SPACE_AT_END_NO_TRIM=This setence will keep its 4 spaces at the end.\    "
98        )
99        pofile = self.prop2po(propsource)
100        pounit = self.singleelement(pofile)
101        assert pounit.source == "This setence will keep its 4 spaces at the end.    "
102
103        propsource = r"SPACE_AT_END_NO_TRIM2=This setence will keep its 4 spaces at the end.\\    "
104        pofile = self.prop2po(propsource)
105        pounit = self.singleelement(pofile)
106        assert pounit.source == "This setence will keep its 4 spaces at the end.\\    "
107
108    def test_tab_at_start_of_value(self):
109        """check that tabs in a property are ignored where appropriate"""
110        propsource = r"property	=	value"
111        pofile = self.prop2po(propsource)
112        pounit = self.singleelement(pofile)
113        assert pounit.getlocations()[0] == "property"
114        assert pounit.source == "value"
115
116    def test_unicode(self):
117        """checks that unicode entries convert properly"""
118        unistring = r"Norsk bokm\u00E5l"
119        propsource = "nb = %s\n" % unistring
120        pofile = self.prop2po(propsource)
121        pounit = self.singleelement(pofile)
122        print(repr(pofile.units[0].target))
123        print(repr(pounit.source))
124        assert pounit.source == "Norsk bokm\u00E5l"
125
126    def test_multiline_escaping(self):
127        """checks that multiline enties can be parsed"""
128        propsource = r"""5093=Unable to connect to your IMAP server. You may have exceeded the maximum number \
129of connections to this server. If so, use the Advanced IMAP Server Settings dialog to \
130reduce the number of cached connections."""
131        pofile = self.prop2po(propsource)
132        print(repr(pofile.units[1].target))
133        assert self.countelements(pofile) == 1
134
135    def test_comments(self):
136        """test to ensure that we take comments from .properties and place them in .po"""
137        propsource = """# Comment
138prefPanel-smime=Security"""
139        pofile = self.prop2po(propsource)
140        pounit = self.singleelement(pofile)
141        assert pounit.getnotes("developer") == "# Comment"
142
143    def test_multiline_comments(self):
144        """test to ensure that we handle multiline comments well"""
145        propsource = """# Comment
146# commenty 2
147
148## @name GENERIC_ERROR
149## @loc none
150prefPanel-smime=
151"""
152        pofile = self.prop2po(propsource)
153        print(bytes(pofile))
154        # header comments:
155        assert b"#. # Comment\n#. # commenty 2" in bytes(pofile)
156        pounit = self.singleelement(pofile)
157        assert pounit.getnotes("developer") == "## @name GENERIC_ERROR\n## @loc none"
158
159    def test_folding_accesskeys(self):
160        """check that we can fold various accesskeys into their associated label (bug #115)"""
161        propsource = r"""cmd_addEngine.label = Add Engines...
162cmd_addEngine.accesskey = A"""
163        pofile = self.prop2po(propsource, personality="mozilla")
164        pounit = self.singleelement(pofile)
165        assert pounit.source == "&Add Engines..."
166
167    def test_dont_translate(self):
168        """check that we know how to ignore don't translate instructions in properties files (bug #116)"""
169        propsource = """# LOCALIZATION NOTE (dont): DONT_TRANSLATE.
170dont=don't translate me
171do=translate me
172"""
173        pofile = self.prop2po(propsource)
174        assert self.countelements(pofile) == 1
175
176    def test_emptyproperty(self):
177        """checks that empty property definitions survive into po file, bug 15"""
178        for delimiter in ["=", ""]:
179            propsource = "# comment\ncredit%s" % delimiter
180            pofile = self.prop2po(propsource)
181            pounit = self.singleelement(pofile)
182            assert pounit.getlocations() == ["credit"]
183            assert pounit.getcontext() == "credit"
184            assert 'msgctxt "credit"' in str(pounit)
185            assert b"#. # comment" in bytes(pofile)
186            assert pounit.source == ""
187
188    def test_emptyproperty_translated(self):
189        """checks that if we translate an empty property it makes it into the PO"""
190        for delimiter in ["=", ""]:
191            proptemplate = "credit%s" % delimiter
192            propsource = "credit=Translators Names"
193            pofile = self.prop2po(propsource, proptemplate)
194            pounit = self.singleelement(pofile)
195            assert pounit.getlocations() == ["credit"]
196            # FIXME we don't seem to get a _: comment but we should
197            # assert pounit.getcontext() == "credit"
198            assert pounit.source == ""
199            assert pounit.target == "Translators Names"
200
201    def test_newlines_in_value(self):
202        """check that we can carry newlines that appear in the property value into the PO"""
203        propsource = """prop=\\nvalue\\n\n"""
204        pofile = self.prop2po(propsource)
205        unit = self.singleelement(pofile)
206        assert unit.source == "\nvalue\n"
207
208    def test_header_comments(self):
209        """check that we can handle comments not directly associated with a property"""
210        propsource = """# Header comment\n\n# Comment\n\nprop=value\n"""
211        pofile = self.prop2po(propsource)
212        unit = self.singleelement(pofile)
213        assert unit.source == "value"
214        assert unit.getnotes("developer") == "# Comment"
215
216    def test_unassociated_comment_order(self):
217        """check that we can handle the order of unassociated comments"""
218        propsource = """# Header comment\n\n# 1st Unassociated comment\n\n# 2nd Connected comment\nprop=value\n"""
219        pofile = self.prop2po(propsource)
220        unit = self.singleelement(pofile)
221        assert unit.source == "value"
222        assert (
223            unit.getnotes("developer")
224            == "# 1st Unassociated comment\n\n# 2nd Connected comment"
225        )
226
227    def test_x_header(self):
228        """Test that we correctly create the custom header entries
229        (accelerators, merge criterion).
230        """
231        propsource = """prop=value\n"""
232
233        outputpo = self.prop2po(propsource, personality="mozilla")
234        assert b"X-Accelerator-Marker" in bytes(outputpo)
235        assert b"X-Merge-On" in bytes(outputpo)
236
237        # Even though the gaia flavour inherrits from mozilla, it should not
238        # get the header
239        outputpo = self.prop2po(propsource, personality="gaia")
240        assert b"X-Accelerator-Marker" not in bytes(outputpo)
241        assert b"X-Merge-On" not in bytes(outputpo)
242
243    def test_gaia_plurals(self):
244        """Test conversion of gaia plural units."""
245        propsource = """
246message-multiedit-header={[ plural(n) ]}
247message-multiedit-header[zero]=Edit
248message-multiedit-header[one]={{ n }} selected
249message-multiedit-header[two]={{ n }} selected
250message-multiedit-header[few]={{ n }} selected
251message-multiedit-header[many]={{ n }} selected
252message-multiedit-header[other]={{ n }} selected
253"""
254        outputpo = self.prop2po(propsource, personality="gaia")
255        pounit = outputpo.units[-1]
256        assert pounit.hasplural()
257        assert pounit.getlocations() == ["message-multiedit-header"]
258
259        print(outputpo)
260        zero_unit = outputpo.units[-2]
261        assert not zero_unit.hasplural()
262        assert zero_unit.source == "Edit"
263
264    def test_successive_gaia_plurals(self):
265        """Test conversion of two successive gaia plural units."""
266        propsource = """
267message-multiedit-header={[ plural(n) ]}
268message-multiedit-header[zero]=Edit
269message-multiedit-header[one]={{ n }} selected
270message-multiedit-header[two]={{ n }} selected
271message-multiedit-header[few]={{ n }} selected
272message-multiedit-header[many]={{ n }} selected
273message-multiedit-header[other]={{ n }} selected
274
275message-multiedit-header2={[ plural(n) ]}
276message-multiedit-header2[zero]=Edit 2
277message-multiedit-header2[one]={{ n }} selected 2
278message-multiedit-header2[two]={{ n }} selected 2
279message-multiedit-header2[few]={{ n }} selected 2
280message-multiedit-header2[many]={{ n }} selected 2
281message-multiedit-header2[other]={{ n }} selected 2
282"""
283        outputpo = self.prop2po(propsource, personality="gaia")
284        pounit = outputpo.units[-1]
285        assert pounit.hasplural()
286        assert pounit.getlocations() == ["message-multiedit-header2"]
287
288        pounit = outputpo.units[-3]
289        assert pounit.hasplural()
290        assert pounit.getlocations() == ["message-multiedit-header"]
291
292        print(outputpo)
293        zero_unit = outputpo.units[-2]
294        assert not zero_unit.hasplural()
295        assert zero_unit.source == "Edit 2"
296
297        zero_unit = outputpo.units[-4]
298        assert not zero_unit.hasplural()
299        assert zero_unit.source == "Edit"
300
301    def test_duplicate_keys(self):
302        """Check that we correctly handle duplicate keys."""
303        source = """
304key=value
305key=value
306"""
307        po_file = self.prop2po(source)
308        assert self.countelements(po_file) == 1
309        po_unit = self.singleelement(po_file)
310        assert po_unit.source == "value"
311
312        source = """
313key=value
314key=another value
315"""
316        po_file = self.prop2po(source)
317        assert self.countelements(po_file) == 2
318        po_unit = po_file.units[1]
319        assert po_unit.source == "value"
320        assert po_unit.getlocations() == ["key"]
321        po_unit = po_file.units[2]
322        assert po_unit.source == "another value"
323        assert po_unit.getlocations() == ["key"]
324
325        source = """
326key1=value
327key2=value
328"""
329        po_file = self.prop2po(source)
330        assert self.countelements(po_file) == 2
331        po_unit = po_file.units[1]
332        assert po_unit.source == "value"
333        assert po_unit.getlocations() == ["key1"]
334        po_unit = po_file.units[2]
335        assert po_unit.source == "value"
336        assert po_unit.getlocations() == ["key2"]
337
338    def test_gwt_plurals(self):
339        """Test conversion of gwt plural units."""
340        propsource = """
341message-multiedit-header={0,number} selected
342message-multiedit-header[none]=Edit
343message-multiedit-header[one]={0,number} selected
344message-multiedit-header[two]={0,number} selected
345message-multiedit-header[few]={0,number} selected
346message-multiedit-header[many]={0,number} selected
347"""
348        outputpo = self.prop2po(propsource, personality="gwt")
349        pounit = outputpo.units[-1]
350        assert pounit.getlocations() == ["message-multiedit-header"]
351
352
353class TestProp2POCommand(test_convert.TestConvertCommand, TestProp2PO):
354    """Tests running actual prop2po commands on files"""
355
356    convertmodule = prop2po
357    defaultoptions = {"progress": "none"}
358
359    def test_help(self, capsys):
360        """tests getting help"""
361        options = super().test_help(capsys)
362        options = self.help_check(options, "-P, --pot")
363        options = self.help_check(options, "-t TEMPLATE, --template=TEMPLATE")
364        options = self.help_check(options, "--personality=TYPE")
365        options = self.help_check(options, "--encoding=ENCODING")
366        options = self.help_check(options, "--duplicates=DUPLICATESTYLE", last=True)
367