Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

702 строки
23 KiB

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