[IMP] res_lang: using a regex instead of custom function,
[odoo/odoo.git] / openerp / addons / base / res / res_lang.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import locale
23 import logging
24 import itertools
25 import re
26
27 from osv import fields, osv
28 from locale import localeconv
29 import tools
30 from tools.safe_eval import safe_eval as eval
31 from tools.translate import _
32
33 class lang(osv.osv):
34     _name = "res.lang"
35     _description = "Languages"
36
37     _disallowed_datetime_patterns = tools.DATETIME_FORMATS_MAP.keys()
38     _disallowed_datetime_patterns.remove('%y') # this one is in fact allowed, just not good practice
39
40     def install_lang(self, cr, uid, **args):
41         lang = tools.config.get('lang')
42         if not lang:
43             return False
44         lang_ids = self.search(cr, uid, [('code','=', lang)])
45         values_obj = self.pool.get('ir.values')
46         if not lang_ids:
47             lang_id = self.load_lang(cr, uid, lang)
48         default_value = values_obj.get(cr, uid, 'default', False, 'res.partner')
49         if not default_value:
50             values_obj.set(cr, uid, 'default', False, 'lang', ['res.partner'], lang)
51         return True
52
53     def load_lang(self, cr, uid, lang, lang_name=None):
54         # create the language with locale information
55         fail = True
56         logger = logging.getLogger('i18n')
57         iso_lang = tools.get_iso_codes(lang)
58         for ln in tools.get_locales(lang):
59             try:
60                 locale.setlocale(locale.LC_ALL, str(ln))
61                 fail = False
62                 break
63             except locale.Error:
64                 continue
65         if fail:
66             lc = locale.getdefaultlocale()[0]
67             msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
68             logger.warning(msg, lang, lc)
69
70         if not lang_name:
71             lang_name = tools.get_languages().get(lang, lang)
72
73
74         def fix_xa0(s):
75             """Fix badly-encoded non-breaking space Unicode character from locale.localeconv(),
76                coercing to utf-8, as some platform seem to output localeconv() in their system
77                encoding, e.g. Windows-1252"""
78             if s == '\xa0':
79                 return '\xc2\xa0'
80             return s
81
82         def fix_datetime_format(format):
83             """Python's strftime supports only the format directives
84                that are available on the platform's libc, so in order to
85                be 100% cross-platform we map to the directives required by
86                the C standard (1989 version), always available on platforms
87                with a C standard implementation."""
88             for pattern, replacement in tools.DATETIME_FORMATS_MAP.iteritems():
89                 format = format.replace(pattern, replacement)
90             return str(format)
91
92         lang_info = {
93             'code': lang,
94             'iso_code': iso_lang,
95             'name': lang_name,
96             'translatable': 1,
97             'date_format' : fix_datetime_format(locale.nl_langinfo(locale.D_FMT)),
98             'time_format' : fix_datetime_format(locale.nl_langinfo(locale.T_FMT)),
99             'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
100             'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
101         }
102         lang_id = False
103         try:
104             lang_id = self.create(cr, uid, lang_info)
105         finally:
106             tools.resetlocale()
107         return lang_id
108
109     def _check_format(self, cr, uid, ids, context=None):
110         for lang in self.browse(cr, uid, ids, context=context):
111             for pattern in self._disallowed_datetime_patterns:
112                 if (lang.time_format and pattern in lang.time_format)\
113                     or (lang.date_format and pattern in lang.date_format):
114                     return False
115         return True
116
117     def _get_default_date_format(self,cursor,user,context={}):
118         return '%m/%d/%Y'
119
120     def _get_default_time_format(self,cursor,user,context={}):
121         return '%H:%M:%S'
122
123     _columns = {
124         'name': fields.char('Name', size=64, required=True),
125         'code': fields.char('Locale Code', size=16, required=True, help='This field is used to set/get locales for user'),
126         'iso_code': fields.char('ISO code', size=16, required=False, help='This ISO code is the name of po files to use for translations'),
127         'translatable': fields.boolean('Translatable'),
128         'active': fields.boolean('Active'),
129         'direction': fields.selection([('ltr', 'Left-to-Right'), ('rtl', 'Right-to-Left')], 'Direction',required=True),
130         'date_format':fields.char('Date Format',size=64,required=True),
131         'time_format':fields.char('Time Format',size=64,required=True),
132         'grouping':fields.char('Separator Format',size=64,required=True,help="The Separator Format should be like [,n] where 0 < n :starting from Unit digit.-1 will end the separation. e.g. [3,2,-1] will represent 106500 to be 1,06,500;[1,2,-1] will represent it to be 106,50,0;[3] will represent it as 106,500. Provided ',' as the thousand separator in each case."),
133         'decimal_point':fields.char('Decimal Separator', size=64,required=True),
134         'thousands_sep':fields.char('Thousands Separator',size=64),
135     }
136     _defaults = {
137         'active': lambda *a: 1,
138         'translatable': lambda *a: 0,
139         'direction': lambda *a: 'ltr',
140         'date_format':_get_default_date_format,
141         'time_format':_get_default_time_format,
142         'grouping':lambda *a: '[]',
143         'decimal_point':lambda *a: '.',
144         'thousands_sep':lambda *a: ',',
145     }
146     _sql_constraints = [
147         ('name_uniq', 'unique (name)', 'The name of the language must be unique !'),
148         ('code_uniq', 'unique (code)', 'The code of the language must be unique !'),
149     ]
150
151     _constraints = [
152         (_check_format, 'Invalid date/time format directive specified. Please refer to the list of allowed directives, displayed when you edit a language.', ['time_format', 'date_format'])
153     ]
154
155     @tools.cache(skiparg=3)
156     def _lang_data_get(self, cr, uid, lang_id, monetary=False):
157         conv = localeconv()
158         lang_obj = self.browse(cr, uid, lang_id)
159         thousands_sep = lang_obj.thousands_sep or conv[monetary and 'mon_thousands_sep' or 'thousands_sep']
160         decimal_point = lang_obj.decimal_point
161         grouping = lang_obj.grouping
162         return (grouping, thousands_sep, decimal_point)
163
164     def write(self, cr, uid, ids, vals, context=None):
165         for lang_id in ids :
166             self._lang_data_get.clear_cache(cr.dbname,lang_id= lang_id)
167         return super(lang, self).write(cr, uid, ids, vals, context)
168
169     def unlink(self, cr, uid, ids, context=None):
170         if context is None:
171             context = {}
172         languages = self.read(cr, uid, ids, ['code','active'], context=context)
173         for language in languages:
174             ctx_lang = context.get('lang')
175             if language['code']=='en_US':
176                 raise osv.except_osv(_('User Error'), _("Base Language 'en_US' can not be deleted !"))
177             if ctx_lang and (language['code']==ctx_lang):
178                 raise osv.except_osv(_('User Error'), _("You cannot delete the language which is User's Preferred Language !"))
179             if language['active']:
180                 raise osv.except_osv(_('User Error'), _("You cannot delete the language which is Active !\nPlease de-activate the language first."))
181             trans_obj = self.pool.get('ir.translation')
182             trans_ids = trans_obj.search(cr, uid, [('lang','=',language['code'])], context=context)
183             trans_obj.unlink(cr, uid, trans_ids, context=context)
184         return super(lang, self).unlink(cr, uid, ids, context=context)
185
186     def format(self, cr, uid, ids, percent, value, grouping=False, monetary=False):
187         """ Format() will return the language-specific output for float values"""
188
189         if percent[0] != '%':
190             raise ValueError("format() must be given exactly one %char format specifier")
191
192         lang_grouping, thousands_sep, decimal_point = self._lang_data_get(cr, uid, ids[0], monetary)
193         eval_lang_grouping = eval(lang_grouping)
194
195         formatted = percent % value
196         # floats and decimal ints need special action!
197         if percent[-1] in 'eEfFgG':
198             seps = 0
199             parts = formatted.split('.')
200
201             if grouping:
202                 parts[0], seps = intersperse(parts[0], eval_lang_grouping, thousands_sep)
203
204             formatted = decimal_point.join(parts)
205             while seps:
206                 sp = formatted.find(' ')
207                 if sp == -1: break
208                 formatted = formatted[:sp] + formatted[sp+1:]
209                 seps -= 1
210         elif percent[-1] in 'diu':
211             if grouping:
212                 formatted = intersperse(formatted, eval_lang_grouping, thousands_sep)[0]
213
214         return formatted
215
216 #    import re, operator
217 #    _percent_re = re.compile(r'%(?:\((?P<key>.*?)\))?'
218 #                             r'(?P<modifiers>[-#0-9 +*.hlL]*?)[eEfFgGdiouxXcrs%]')
219
220 lang()
221
222 def original_group(s, grouping, thousands_sep=''):
223
224     if not grouping:
225         return (s, 0)
226
227     result = ""
228     seps = 0
229     spaces = ""
230
231     if s[-1] == ' ':
232         sp = s.find(' ')
233         spaces = s[sp:]
234         s = s[:sp]
235
236     while s and grouping:
237         # if grouping is -1, we are done
238         if grouping[0] == -1:
239             break
240         # 0: re-use last group ad infinitum
241         elif grouping[0] != 0:
242             #process last group
243             group = grouping[0]
244             grouping = grouping[1:]
245         if result:
246             result = s[-group:] + thousands_sep + result
247             seps += 1
248         else:
249             result = s[-group:]
250         s = s[:-group]
251         if s and s[-1] not in "0123456789":
252             # the leading string is only spaces and signs
253             return s + result + spaces, seps
254     if not result:
255         return s + spaces, seps
256     if s:
257         result = s + thousands_sep + result
258         seps += 1
259     return result + spaces, seps
260
261 def split(l, counts):
262     """
263
264     >>> split("hello world", [])
265     ['hello world']
266     >>> split("hello world", [1])
267     ['h', 'ello world']
268     >>> split("hello world", [2])
269     ['he', 'llo world']
270     >>> split("hello world", [2,3])
271     ['he', 'llo', ' world']
272     >>> split("hello world", [2,3,0])
273     ['he', 'llo', ' wo', 'rld']
274     >>> split("hello world", [2,-1,3])
275     ['he', 'llo world']
276
277     """
278     res = []
279     saved_count = len(l) # count to use when encoutering a zero
280     for count in counts:
281         if count == -1:
282             break
283         if count == 0:
284             while l:
285                 res.append(l[:saved_count])
286                 l = l[saved_count:]
287             break
288         res.append(l[:count])
289         l = l[count:]
290         saved_count = count
291     if l:
292         res.append(l)
293     return res
294
295 intersperse_pat = re.compile('([^0-9]*)([^ ]*)(.*)')
296
297 def intersperse(string, counts, separator=''):
298     """
299
300     See the asserts below for examples.
301
302     """
303     left, rest, right = intersperse_pat.match(string).groups()
304     def reverse(s): return s[::-1]
305     splits = split(reverse(rest), counts)
306     res = separator.join(map(reverse, reverse(splits)))
307     return left + res + right, len(splits) > 0 and len(splits) -1 or 0
308
309 # TODO rewrite this with a unit test library
310 def _group_examples():
311     for g in [original_group, intersperse]:
312         # print "asserts on", g.func_name
313         assert g("", []) == ("", 0)
314         assert g("0", []) == ("0", 0)
315         assert g("012", []) == ("012", 0)
316         assert g("1", []) == ("1", 0)
317         assert g("12", []) == ("12", 0)
318         assert g("123", []) == ("123", 0)
319         assert g("1234", []) == ("1234", 0)
320         assert g("123456789", []) == ("123456789", 0)
321         assert g("&ab%#@1", []) == ("&ab%#@1", 0)
322
323         assert g("0", []) == ("0", 0)
324         assert g("0", [1]) == ("0", 0)
325         assert g("0", [2]) == ("0", 0)
326         assert g("0", [200]) == ("0", 0)
327
328         # breaks original_group:
329         if g.func_name == 'intersperse':
330             assert g("12345678", [0], '.') == ('12345678', 0)
331             assert g("", [1], '.') == ('', 0)
332         assert g("12345678", [1], '.') == ('1234567.8', 1)
333         assert g("12345678", [1], '.') == ('1234567.8', 1)
334         assert g("12345678", [2], '.') == ('123456.78', 1)
335         assert g("12345678", [2,1], '.') == ('12345.6.78', 2)
336         assert g("12345678", [2,0], '.') == ('12.34.56.78', 3)
337         assert g("12345678", [-1,2], '.') == ('12345678', 0)
338         assert g("12345678", [2,-1], '.') == ('123456.78', 1)
339         assert g("12345678", [2,0,1], '.') == ('12.34.56.78', 3)
340         assert g("12345678", [2,0,0], '.') == ('12.34.56.78', 3)
341         assert g("12345678", [2,0,-1], '.') == ('12.34.56.78', 3)
342
343
344     assert original_group("abc1234567xy", [2], '.') == ('abc1234567.xy', 1)
345     assert original_group("abc1234567xy8", [2], '.') == ('abc1234567xy8', 0) # difference here...
346     assert original_group("abc12", [3], '.') == ('abc12', 0)
347     assert original_group("abc12", [2], '.') == ('abc12', 0)
348     assert original_group("abc12", [1], '.') == ('abc1.2', 1)
349
350     assert intersperse("abc1234567xy", [2], '.') == ('abc1234567.xy', 1)
351     assert intersperse("abc1234567xy8", [2], '.') == ('abc1234567x.y8', 1) # ... w.r.t. here.
352     assert intersperse("abc12", [3], '.') == ('abc12', 0)
353     assert intersperse("abc12", [2], '.') == ('abc12', 0)
354     assert intersperse("abc12", [1], '.') == ('abc1.2', 1)
355
356
357 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: