選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

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