Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 
 

685 rindas
22 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. """
  5. frappe.translate
  6. ~~~~~~~~~~~~~~~~
  7. Translation tools for frappe
  8. """
  9. import frappe, os, re, codecs, json
  10. from frappe.model.utils import render_include, InvalidIncludePath
  11. from frappe.utils import strip
  12. from jinja2 import TemplateError
  13. import itertools, operator
  14. def guess_language(lang_list=None):
  15. """Set `frappe.local.lang` from HTTP headers at beginning of request"""
  16. lang_codes = frappe.request.accept_languages.values()
  17. if not lang_codes:
  18. return frappe.local.lang
  19. guess = None
  20. if not lang_list:
  21. lang_list = get_all_languages() or []
  22. for l in lang_codes:
  23. code = l.strip()
  24. if code in lang_list or code == "en":
  25. guess = code
  26. break
  27. # check if parent language (pt) is setup, if variant (pt-BR)
  28. if "-" in code:
  29. code = code.split("-")[0]
  30. if code in lang_list:
  31. guess = code
  32. break
  33. return guess or frappe.local.lang
  34. def get_user_lang(user=None):
  35. """Set frappe.local.lang from user preferences on session beginning or resumption"""
  36. if not user:
  37. user = frappe.session.user
  38. # via cache
  39. lang = frappe.cache().hget("lang", user)
  40. if not lang:
  41. # if defined in user profile
  42. user_lang = frappe.db.get_value("User", user, "language")
  43. if user_lang and user_lang!="Loading...":
  44. lang = get_lang_code(user_lang)
  45. else:
  46. default_lang = frappe.db.get_default("lang")
  47. lang = get_lang_code(default_lang)
  48. if not lang:
  49. lang = frappe.local.lang or 'en'
  50. frappe.cache().hset("lang", user, lang)
  51. return lang
  52. def get_lang_code(lang):
  53. return get_lang_dict().get(lang, lang)
  54. def set_default_language(language):
  55. """Set Global default language"""
  56. lang = get_lang_dict().get(language, language)
  57. frappe.db.set_default("lang", lang)
  58. frappe.local.lang = lang
  59. def get_all_languages():
  60. """Returns all language codes ar, ch etc"""
  61. return [a.split()[0] for a in get_lang_info()]
  62. def get_lang_dict():
  63. """Returns all languages in dict format, full name is the key e.g. `{"english":"en"}`"""
  64. return dict([[a[1], a[0]] for a in [a.split(None, 1) for a in get_lang_info()]])
  65. def get_language_from_code(lang):
  66. return dict(a.split(None, 1) for a in get_lang_info()).get(lang)
  67. def get_lang_info():
  68. """Returns a listified version of `apps/languages.txt`"""
  69. return frappe.cache().get_value("langinfo",
  70. lambda:frappe.get_file_items(os.path.join(frappe.local.sites_path, "languages.txt")))
  71. def get_dict(fortype, name=None):
  72. """Returns translation dict for a type of object.
  73. :param fortype: must be one of `doctype`, `page`, `report`, `include`, `jsfile`, `boot`
  74. :param name: name of the document for which assets are to be returned.
  75. """
  76. fortype = fortype.lower()
  77. cache = frappe.cache()
  78. asset_key = fortype + ":" + (name or "-")
  79. translation_assets = cache.hget("translation_assets", frappe.local.lang) or {}
  80. if not asset_key in translation_assets:
  81. if fortype=="doctype":
  82. messages = get_messages_from_doctype(name)
  83. elif fortype=="page":
  84. messages = get_messages_from_page(name)
  85. elif fortype=="report":
  86. messages = get_messages_from_report(name)
  87. elif fortype=="include":
  88. messages = get_messages_from_include_files()
  89. elif fortype=="jsfile":
  90. messages = get_messages_from_file(name)
  91. elif fortype=="boot":
  92. messages = get_messages_from_include_files()
  93. messages += frappe.db.sql("select 'DocType:', name from tabDocType")
  94. messages += frappe.db.sql("select 'Role:', name from tabRole")
  95. messages += frappe.db.sql("select 'Module:', name from `tabModule Def`")
  96. messages += frappe.db.sql("select 'Module:', label from `tabDesktop Icon` where standard=1 or owner=%s",
  97. frappe.session.user)
  98. translation_assets[asset_key] = make_dict_from_messages(messages)
  99. translation_assets[asset_key].update(get_dict_from_hooks(fortype, name))
  100. cache.hset("translation_assets", frappe.local.lang, translation_assets)
  101. return translation_assets[asset_key]
  102. def get_dict_from_hooks(fortype, name):
  103. translated_dict = {}
  104. hooks = frappe.get_hooks("get_translated_dict")
  105. for (hook_fortype, fortype_name) in hooks:
  106. if hook_fortype == fortype and fortype_name == name:
  107. for method in hooks[(hook_fortype, fortype_name)]:
  108. translated_dict.update(frappe.get_attr(method)())
  109. return translated_dict
  110. def add_lang_dict(code):
  111. """Extracts messages and returns Javascript code snippet to be appened at the end
  112. of the given script
  113. :param code: Javascript code snippet to which translations needs to be appended."""
  114. messages = extract_messages_from_code(code)
  115. messages = [message for pos, message in messages]
  116. code += "\n\n$.extend(frappe._messages, %s)" % json.dumps(make_dict_from_messages(messages))
  117. return code
  118. def make_dict_from_messages(messages, full_dict=None):
  119. """Returns translated messages as a dict in Language specified in `frappe.local.lang`
  120. :param messages: List of untranslated messages
  121. """
  122. out = {}
  123. if full_dict==None:
  124. full_dict = get_full_dict(frappe.local.lang)
  125. for m in messages:
  126. if m[1] in full_dict:
  127. out[m[1]] = full_dict[m[1]]
  128. return out
  129. def get_lang_js(fortype, name):
  130. """Returns code snippet to be appended at the end of a JS script.
  131. :param fortype: Type of object, e.g. `DocType`
  132. :param name: Document name
  133. """
  134. return "\n\n$.extend(frappe._messages, %s)" % json.dumps(get_dict(fortype, name))
  135. def get_full_dict(lang):
  136. """Load and return the entire translations dictionary for a language from :meth:`frape.cache`
  137. :param lang: Language Code, e.g. `hi`
  138. """
  139. if not lang:
  140. return {}
  141. # found in local, return!
  142. if frappe.local.lang_full_dict is not None:
  143. return frappe.local.lang_full_dict
  144. frappe.local.lang_full_dict = frappe.cache().hget("lang_full_dict", lang)
  145. if frappe.local.lang_full_dict is None:
  146. frappe.local.lang_full_dict = load_lang(lang)
  147. # only cache file translations in this
  148. frappe.cache().hset("lang_full_dict", lang, frappe.local.lang_full_dict)
  149. try:
  150. # get user specific transaltion data
  151. user_translations = get_user_translations(lang)
  152. except Exception:
  153. user_translations = None
  154. if user_translations:
  155. frappe.local.lang_full_dict.update(user_translations)
  156. return frappe.local.lang_full_dict
  157. def load_lang(lang, apps=None):
  158. """Combine all translations from `.csv` files in all `apps`"""
  159. out = {}
  160. for app in (apps or frappe.get_all_apps(True)):
  161. path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv")
  162. out.update(get_translation_dict_from_file(path, lang, app))
  163. return out
  164. def get_translation_dict_from_file(path, lang, app):
  165. """load translation dict from given path"""
  166. cleaned = {}
  167. if os.path.exists(path):
  168. csv_content = read_csv_file(path)
  169. for item in csv_content:
  170. if len(item)==3:
  171. # with file and line numbers
  172. cleaned[item[1]] = strip(item[2])
  173. elif len(item)==2:
  174. cleaned[item[0]] = strip(item[1])
  175. else:
  176. raise Exception("Bad translation in '{app}' for language '{lang}': {values}".format(
  177. app=app, lang=lang, values=repr(item).encode("utf-8")
  178. ))
  179. return cleaned
  180. def get_user_translations(lang):
  181. out = frappe.cache().hget('lang_user_translations', lang)
  182. if out is None:
  183. out = {}
  184. for fields in frappe.get_all('Translation',
  185. fields= ["source_name", "target_name"],filters={'language_code': lang}):
  186. out.update({fields.source_name: fields.target_name})
  187. frappe.cache().hset('lang_user_translations', lang, out)
  188. return out
  189. # def get_user_translation_key():
  190. # return 'lang_user_translations:{0}'.format(frappe.local.site)
  191. def clear_cache():
  192. """Clear all translation assets from :meth:`frappe.cache`"""
  193. cache = frappe.cache()
  194. cache.delete_key("langinfo")
  195. cache.delete_key("lang_full_dict")
  196. cache.delete_key("translation_assets")
  197. def get_messages_for_app(app):
  198. """Returns all messages (list) for a specified `app`"""
  199. messages = []
  200. modules = ", ".join(['"{}"'.format(m.title().replace("_", " ")) \
  201. for m in frappe.local.app_modules[app]])
  202. # doctypes
  203. if modules:
  204. for name in frappe.db.sql_list("""select name from tabDocType
  205. where module in ({})""".format(modules)):
  206. messages.extend(get_messages_from_doctype(name))
  207. # pages
  208. for name, title in frappe.db.sql("""select name, title from tabPage
  209. where module in ({})""".format(modules)):
  210. messages.append((None, title or name))
  211. messages.extend(get_messages_from_page(name))
  212. # reports
  213. for name in frappe.db.sql_list("""select tabReport.name from tabDocType, tabReport
  214. where tabReport.ref_doctype = tabDocType.name
  215. and tabDocType.module in ({})""".format(modules)):
  216. messages.append((None, name))
  217. messages.extend(get_messages_from_report(name))
  218. for i in messages:
  219. if not isinstance(i, tuple):
  220. raise Exception
  221. # workflow based on app.hooks.fixtures
  222. messages.extend(get_messages_from_workflow(app_name=app))
  223. # custom fields based on app.hooks.fixtures
  224. messages.extend(get_messages_from_custom_fields(app_name=app))
  225. # app_include_files
  226. messages.extend(get_all_messages_from_js_files(app))
  227. # server_messages
  228. messages.extend(get_server_messages(app))
  229. return deduplicate_messages(messages)
  230. def get_messages_from_doctype(name):
  231. """Extract all translatable messages for a doctype. Includes labels, Python code,
  232. Javascript code, html templates"""
  233. messages = []
  234. meta = frappe.get_meta(name)
  235. messages = [meta.name, meta.module]
  236. if meta.description:
  237. messages.append(meta.description)
  238. # translations of field labels, description and options
  239. for d in meta.get("fields"):
  240. messages.extend([d.label, d.description])
  241. if d.fieldtype=='Select' and d.options:
  242. options = d.options.split('\n')
  243. if not "icon" in options[0]:
  244. messages.extend(options)
  245. # translations of roles
  246. for d in meta.get("permissions"):
  247. if d.role:
  248. messages.append(d.role)
  249. messages = [message for message in messages if message]
  250. messages = [('DocType: ' + name, message) for message in messages if is_translatable(message)]
  251. # extract from js, py files
  252. doctype_file_path = frappe.get_module_path(meta.module, "doctype", meta.name, meta.name)
  253. messages.extend(get_messages_from_file(doctype_file_path + ".js"))
  254. messages.extend(get_messages_from_file(doctype_file_path + "_list.js"))
  255. messages.extend(get_messages_from_file(doctype_file_path + "_list.html"))
  256. messages.extend(get_messages_from_file(doctype_file_path + "_calendar.js"))
  257. # workflow based on doctype
  258. messages.extend(get_messages_from_workflow(doctype=name))
  259. return messages
  260. def get_messages_from_workflow(doctype=None, app_name=None):
  261. assert doctype or app_name, 'doctype or app_name should be provided'
  262. # translations for Workflows
  263. workflows = []
  264. if doctype:
  265. workflows = frappe.get_all('Workflow', filters={'document_type': doctype})
  266. else:
  267. fixtures = frappe.get_hooks('fixtures', app_name=app_name) or []
  268. for fixture in fixtures:
  269. if isinstance(fixture, basestring) and fixture == 'Worflow':
  270. workflows = frappe.get_all('Workflow')
  271. break
  272. elif isinstance(fixture, dict) and fixture.get('dt', fixture.get('doctype')) == 'Workflow':
  273. workflows.extend(frappe.get_all('Workflow', filters=fixture.get('filters')))
  274. messages = []
  275. for w in workflows:
  276. states = frappe.db.sql(
  277. 'select distinct state from `tabWorkflow Document State` where parent=%s',
  278. (w['name'],), as_dict=True)
  279. messages.extend([('Workflow: ' + w['name'], state['state']) for state in states if is_translatable(state['state'])])
  280. states = frappe.db.sql(
  281. 'select distinct message from `tabWorkflow Document State` where parent=%s and message is not null',
  282. (w['name'],), as_dict=True)
  283. messages.extend([("Workflow: " + w['name'], states['message'])
  284. for state in states if is_translatable(state['state'])])
  285. actions = frappe.db.sql(
  286. 'select distinct action from `tabWorkflow Transition` where parent=%s',
  287. (w['name'],), as_dict=True)
  288. messages.extend([("Workflow: " + w['name'], action['action']) \
  289. for action in actions if is_translatable(action['action'])])
  290. return messages
  291. def get_messages_from_custom_fields(app_name):
  292. fixtures = frappe.get_hooks('fixtures', app_name=app_name) or []
  293. custom_fields = []
  294. for fixture in fixtures:
  295. if isinstance(fixture, basestring) and fixture == 'Custom Field':
  296. custom_fields = frappe.get_all('Custom Field')
  297. break
  298. elif isinstance(fixture, dict) and fixture.get('dt', fixture.get('doctype')) == 'Custom Field':
  299. custom_fields.extend(frappe.get_all('Custom Field', filters=fixture.get('filters'),
  300. fields=['name','label', 'description', 'fieldtype', 'options']))
  301. messages = []
  302. for cf in custom_fields:
  303. for prop in ('label', 'description'):
  304. if not cf.get(prop) or not is_translatable(cf[prop]):
  305. continue
  306. messages.append(('Custom Field - {}: {}'.format(prop, cf['name']), cf[prop]))
  307. if cf['fieldtype'] == 'Selection' and cf.get('options'):
  308. for option in cf['options'].split('\n'):
  309. if option and 'icon' not in option and is_translatable(option):
  310. messages.append(('Custom Field - Description: ' + cf['name'], option))
  311. return messages
  312. def get_messages_from_page(name):
  313. """Returns all translatable strings from a :class:`frappe.core.doctype.Page`"""
  314. return _get_messages_from_page_or_report("Page", name)
  315. def get_messages_from_report(name):
  316. """Returns all translatable strings from a :class:`frappe.core.doctype.Report`"""
  317. report = frappe.get_doc("Report", name)
  318. messages = _get_messages_from_page_or_report("Report", name,
  319. frappe.db.get_value("DocType", report.ref_doctype, "module"))
  320. # TODO position here!
  321. if report.query:
  322. messages.extend([(None, message) for message in re.findall('"([^:,^"]*):', report.query) if is_translatable(message)])
  323. messages.append((None,report.report_name))
  324. return messages
  325. def _get_messages_from_page_or_report(doctype, name, module=None):
  326. if not module:
  327. module = frappe.db.get_value(doctype, name, "module")
  328. doc_path = frappe.get_module_path(module, doctype, name)
  329. messages = get_messages_from_file(os.path.join(doc_path, frappe.scrub(name) +".py"))
  330. if os.path.exists(doc_path):
  331. for filename in os.listdir(doc_path):
  332. if filename.endswith(".js") or filename.endswith(".html"):
  333. messages += get_messages_from_file(os.path.join(doc_path, filename))
  334. return messages
  335. def get_server_messages(app):
  336. """Extracts all translatable strings (tagged with :func:`frappe._`) from Python modules
  337. inside an app"""
  338. messages = []
  339. for basepath, folders, files in os.walk(frappe.get_pymodule_path(app)):
  340. for dontwalk in (".git", "public", "locale"):
  341. if dontwalk in folders: folders.remove(dontwalk)
  342. for f in files:
  343. if f.endswith(".py") or f.endswith(".html") or f.endswith(".js"):
  344. messages.extend(get_messages_from_file(os.path.join(basepath, f)))
  345. return messages
  346. def get_messages_from_include_files(app_name=None):
  347. """Returns messages from js files included at time of boot like desk.min.js for desk and web"""
  348. messages = []
  349. for file in (frappe.get_hooks("app_include_js", app_name=app_name) or []) + (frappe.get_hooks("web_include_js", app_name=app_name) or []):
  350. messages.extend(get_messages_from_file(os.path.join(frappe.local.sites_path, file)))
  351. return messages
  352. def get_all_messages_from_js_files(app_name=None):
  353. """Extracts all translatable strings from app `.js` files"""
  354. messages = []
  355. for app in ([app_name] if app_name else frappe.get_installed_apps()):
  356. if os.path.exists(frappe.get_app_path(app, "public")):
  357. for basepath, folders, files in os.walk(frappe.get_app_path(app, "public")):
  358. if "frappe/public/js/lib" in basepath:
  359. continue
  360. for fname in files:
  361. if fname.endswith(".js") or fname.endswith(".html"):
  362. messages.extend(get_messages_from_file(os.path.join(basepath, fname)))
  363. return messages
  364. def get_messages_from_file(path):
  365. """Returns a list of transatable strings from a code file
  366. :param path: path of the code file
  367. """
  368. apps_path = get_bench_dir()
  369. if os.path.exists(path):
  370. with open(path, 'r') as sourcefile:
  371. return [(os.path.relpath(" +".join([path, str(pos)]), apps_path),
  372. message) for pos, message in extract_messages_from_code(sourcefile.read(), path.endswith(".py"))]
  373. else:
  374. # print "Translate: {0} missing".format(os.path.abspath(path))
  375. return []
  376. def extract_messages_from_code(code, is_py=False):
  377. """Extracts translatable srings from a code file
  378. :param code: code from which translatable files are to be extracted
  379. :param is_py: include messages in triple quotes e.g. `_('''message''')`"""
  380. try:
  381. code = render_include(code)
  382. except (TemplateError, ImportError, InvalidIncludePath):
  383. # Exception will occur when it encounters John Resig's microtemplating code
  384. pass
  385. messages = []
  386. messages += [(m.start(), m.groups()[0]) for m in re.compile('_\("([^"]*)"').finditer(code)]
  387. messages += [(m.start(), m.groups()[0]) for m in re.compile("_\('([^']*)'").finditer(code)]
  388. if is_py:
  389. messages += [(m.start(), m.groups()[0]) for m in re.compile('_\("{3}([^"]*)"{3}.*\)').finditer(code)]
  390. messages = [(pos, message) for pos, message in messages if is_translatable(message)]
  391. return pos_to_line_no(messages, code)
  392. def is_translatable(m):
  393. if re.search("[a-zA-Z]", m) and not m.startswith("icon-") and not m.endswith("px") and not m.startswith("eval:"):
  394. return True
  395. return False
  396. def pos_to_line_no(messages, code):
  397. ret = []
  398. messages = sorted(messages, key=lambda x: x[0])
  399. newlines = [m.start() for m in re.compile('\\n').finditer(code)]
  400. line = 1
  401. newline_i = 0
  402. for pos, message in messages:
  403. while newline_i < len(newlines) and pos > newlines[newline_i]:
  404. line+=1
  405. newline_i+= 1
  406. ret.append((line, message))
  407. return ret
  408. def read_csv_file(path):
  409. """Read CSV file and return as list of list
  410. :param path: File path"""
  411. from csv import reader
  412. with codecs.open(path, 'r', 'utf-8') as msgfile:
  413. data = msgfile.read()
  414. # for japanese! #wtf
  415. data = data.replace(chr(28), "").replace(chr(29), "")
  416. data = reader([r.encode('utf-8') for r in data.splitlines()])
  417. newdata = [[unicode(val, 'utf-8') for val in row] for row in data]
  418. return newdata
  419. def write_csv_file(path, app_messages, lang_dict):
  420. """Write translation CSV file.
  421. :param path: File path, usually `[app]/translations`.
  422. :param app_messages: Translatable strings for this app.
  423. :param lang_dict: Full translated dict.
  424. """
  425. app_messages.sort(lambda x,y: cmp(x[1], y[1]))
  426. from csv import writer
  427. with open(path, 'wb') as msgfile:
  428. w = writer(msgfile, lineterminator='\n')
  429. for p, m in app_messages:
  430. t = lang_dict.get(m, '')
  431. # strip whitespaces
  432. t = re.sub('{\s?([0-9]+)\s?}', "{\g<1>}", t)
  433. w.writerow([p.encode('utf-8') if p else '', m.encode('utf-8'), t.encode('utf-8')])
  434. def get_untranslated(lang, untranslated_file, get_all=False):
  435. """Returns all untranslated strings for a language and writes in a file
  436. :param lang: Language code.
  437. :param untranslated_file: Output file path.
  438. :param get_all: Return all strings, translated or not."""
  439. clear_cache()
  440. apps = frappe.get_all_apps(True)
  441. messages = []
  442. untranslated = []
  443. for app in apps:
  444. messages.extend(get_messages_for_app(app))
  445. messages = deduplicate_messages(messages)
  446. def escape_newlines(s):
  447. return (s.replace("\\\n", "|||||")
  448. .replace("\\n", "||||")
  449. .replace("\n", "|||"))
  450. if get_all:
  451. print str(len(messages)) + " messages"
  452. with open(untranslated_file, "w") as f:
  453. for m in messages:
  454. # replace \n with ||| so that internal linebreaks don't get split
  455. f.write((escape_newlines(m[1]) + os.linesep).encode("utf-8"))
  456. else:
  457. full_dict = get_full_dict(lang)
  458. for m in messages:
  459. if not full_dict.get(m[1]):
  460. untranslated.append(m[1])
  461. if untranslated:
  462. print str(len(untranslated)) + " missing translations of " + str(len(messages))
  463. with open(untranslated_file, "w") as f:
  464. for m in untranslated:
  465. # replace \n with ||| so that internal linebreaks don't get split
  466. f.write((escape_newlines(m) + os.linesep).encode("utf-8"))
  467. else:
  468. print "all translated!"
  469. def update_translations(lang, untranslated_file, translated_file):
  470. """Update translations from a source and target file for a given language.
  471. :param lang: Language code (e.g. `en`).
  472. :param untranslated_file: File path with the messages in English.
  473. :param translated_file: File path with messages in language to be updated."""
  474. clear_cache()
  475. full_dict = get_full_dict(lang)
  476. def restore_newlines(s):
  477. return (s.replace("|||||", "\\\n")
  478. .replace("| | | | |", "\\\n")
  479. .replace("||||", "\\n")
  480. .replace("| | | |", "\\n")
  481. .replace("|||", "\n")
  482. .replace("| | |", "\n"))
  483. translation_dict = {}
  484. for key, value in zip(frappe.get_file_items(untranslated_file, ignore_empty_lines=False),
  485. frappe.get_file_items(translated_file, ignore_empty_lines=False)):
  486. # undo hack in get_untranslated
  487. translation_dict[restore_newlines(key)] = restore_newlines(value)
  488. full_dict.update(translation_dict)
  489. for app in frappe.get_all_apps(True):
  490. write_translations_file(app, lang, full_dict)
  491. def import_translations(lang, path):
  492. """Import translations from file in standard format"""
  493. clear_cache()
  494. full_dict = get_full_dict(lang)
  495. full_dict.update(get_translation_dict_from_file(path, lang, 'import'))
  496. for app in frappe.get_all_apps(True):
  497. write_translations_file(app, lang, full_dict)
  498. def rebuild_all_translation_files():
  499. """Rebuild all translation files: `[app]/translations/[lang].csv`."""
  500. for lang in get_all_languages():
  501. for app in frappe.get_all_apps():
  502. write_translations_file(app, lang)
  503. def write_translations_file(app, lang, full_dict=None, app_messages=None):
  504. """Write a translation file for a given language.
  505. :param app: `app` for which translations are to be written.
  506. :param lang: Language code.
  507. :param full_dict: Full translated language dict (optional).
  508. :param app_messages: Source strings (optional).
  509. """
  510. if not app_messages:
  511. app_messages = get_messages_for_app(app)
  512. if not app_messages:
  513. return
  514. tpath = frappe.get_pymodule_path(app, "translations")
  515. frappe.create_folder(tpath)
  516. write_csv_file(os.path.join(tpath, lang + ".csv"),
  517. app_messages, full_dict or get_full_dict(lang))
  518. def send_translations(translation_dict):
  519. """Append translated dict in `frappe.local.response`"""
  520. if "__messages" not in frappe.local.response:
  521. frappe.local.response["__messages"] = {}
  522. frappe.local.response["__messages"].update(translation_dict)
  523. def deduplicate_messages(messages):
  524. ret = []
  525. op = operator.itemgetter(1)
  526. messages = sorted(messages, key=op)
  527. for k, g in itertools.groupby(messages, op):
  528. ret.append(g.next())
  529. return ret
  530. def get_bench_dir():
  531. return os.path.join(frappe.__file__, '..', '..', '..', '..')
  532. def rename_language(old_name, new_name):
  533. language_in_system_settings = frappe.db.get_single_value("System Settings", "language")
  534. if language_in_system_settings == old_name:
  535. frappe.db.set_value("System Settings", "System Settings", "language", new_name)
  536. frappe.db.sql("""update `tabUser` set language=%(new_name)s where language=%(old_name)s""",
  537. { "old_name": old_name, "new_name": new_name })