Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

522 linhas
17 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.utils.jinja import render_include
  11. from jinja2 import TemplateError
  12. import itertools, operator
  13. def guess_language(lang_codes):
  14. """Set `frappe.local.lang` from HTTP headers at beginning of request"""
  15. if not lang_codes:
  16. return frappe.local.lang
  17. guess = None
  18. lang_list = get_all_languages() or []
  19. for l in lang_codes:
  20. code = l.strip()
  21. if code in lang_list:
  22. guess = code
  23. break
  24. # check if parent language (pt) is setup, if variant (pt-BR)
  25. if "-" in code:
  26. code = code.split("-")[0]
  27. if code in lang_list:
  28. guess = code
  29. break
  30. return guess or frappe.local.lang
  31. def get_user_lang(user=None):
  32. """Set frappe.local.lang from user preferences on session beginning or resumption"""
  33. if not user:
  34. user = frappe.session.user
  35. # via cache
  36. lang = frappe.cache().get_value("lang", user=user)
  37. if not lang:
  38. # if defined in user profile
  39. user_lang = frappe.db.get_value("User", user, "language")
  40. if user_lang and user_lang!="Loading...":
  41. lang = get_lang_dict().get(user_lang) or frappe.local.lang
  42. else:
  43. default_lang = frappe.db.get_default("lang")
  44. lang = default_lang or frappe.local.lang
  45. frappe.cache().set_value("lang", lang or "en", user=user)
  46. return lang
  47. def set_default_language(language):
  48. """Set Global default language"""
  49. lang = get_lang_dict()[language]
  50. frappe.db.set_default("lang", lang)
  51. frappe.local.lang = lang
  52. def get_all_languages():
  53. """Returns all language codes ar, ch etc"""
  54. return [a.split()[0] for a in get_lang_info()]
  55. def get_lang_dict():
  56. """Returns all languages in dict format, full name is the key e.g. `{"english":"en"}`"""
  57. return dict([[a[1], a[0]] for a in [a.split(None, 1) for a in get_lang_info()]])
  58. def get_language_from_code(lang):
  59. return dict(a.split(None, 1) for a in get_lang_info()).get(lang)
  60. def get_lang_info():
  61. """Returns a listified version of `apps/languages.txt`"""
  62. return frappe.cache().get_value("langinfo",
  63. lambda:frappe.get_file_items(os.path.join(frappe.local.sites_path, "languages.txt")))
  64. def get_dict(fortype, name=None):
  65. """Returns translation dict for a type of object.
  66. :param fortype: must be one of `doctype`, `page`, `report`, `include`, `jsfile`, `boot`
  67. :param name: name of the document for which assets are to be returned.
  68. """
  69. fortype = fortype.lower()
  70. cache = frappe.cache()
  71. cache_key = "translation_assets:" + (frappe.local.lang or "en")
  72. asset_key = fortype + ":" + (name or "-")
  73. translation_assets = cache.get_value(cache_key) or {}
  74. if not asset_key in translation_assets:
  75. if fortype=="doctype":
  76. messages = get_messages_from_doctype(name)
  77. elif fortype=="page":
  78. messages = get_messages_from_page(name)
  79. elif fortype=="report":
  80. messages = get_messages_from_report(name)
  81. elif fortype=="include":
  82. messages = get_messages_from_include_files()
  83. elif fortype=="jsfile":
  84. messages = get_messages_from_file(name)
  85. elif fortype=="boot":
  86. messages = get_messages_from_include_files()
  87. messages += frappe.db.sql("select 'DocType:', name from tabDocType")
  88. messages += frappe.db.sql("select 'Role:', name from tabRole")
  89. messages += frappe.db.sql("select 'Module:', name from `tabModule Def`")
  90. translation_assets[asset_key] = make_dict_from_messages(messages)
  91. translation_assets[asset_key].update(get_dict_from_hooks(fortype, name))
  92. cache.set_value(cache_key, translation_assets)
  93. return translation_assets[asset_key]
  94. def get_dict_from_hooks(fortype, name):
  95. translated_dict = {}
  96. hooks = frappe.get_hooks("get_translated_dict")
  97. for (hook_fortype, fortype_name) in hooks:
  98. if hook_fortype == fortype and fortype_name == name:
  99. for method in hooks[(hook_fortype, fortype_name)]:
  100. translated_dict.update(frappe.get_attr(method)())
  101. return translated_dict
  102. def add_lang_dict(code):
  103. """Extracts messages and returns Javascript code snippet to be appened at the end
  104. of the given script
  105. :param code: Javascript code snippet to which translations needs to be appended."""
  106. messages = extract_messages_from_code(code)
  107. messages = [message for pos, message in messages]
  108. code += "\n\n$.extend(frappe._messages, %s)" % json.dumps(make_dict_from_messages(messages))
  109. return code
  110. def make_dict_from_messages(messages, full_dict=None):
  111. """Returns translated messages as a dict in Language specified in `frappe.local.lang`
  112. :param messages: List of untranslated messages
  113. """
  114. out = {}
  115. if full_dict==None:
  116. full_dict = get_full_dict(frappe.local.lang)
  117. for m in messages:
  118. if m[1] in full_dict:
  119. out[m[1]] = full_dict[m[1]]
  120. return out
  121. def get_lang_js(fortype, name):
  122. """Returns code snippet to be appended at the end of a JS script.
  123. :param fortype: Type of object, e.g. `DocType`
  124. :param name: Document name
  125. """
  126. return "\n\n$.extend(frappe._messages, %s)" % json.dumps(get_dict(fortype, name))
  127. def get_full_dict(lang):
  128. """Load and return the entire translations dictionary for a language from :meth:`frape.cache`
  129. :param lang: Language Code, e.g. `hi`
  130. """
  131. if lang == "en":
  132. return {}
  133. if not frappe.local.lang_full_dict:
  134. frappe.local.lang_full_dict = frappe.cache().get_value("lang:" + lang)
  135. if not frappe.local.lang_full_dict:
  136. # cache lang
  137. frappe.cache().set_value("lang:" + lang, frappe.local.lang_full_dict)
  138. frappe.local.lang_full_dict = load_lang(lang)
  139. return frappe.local.lang_full_dict
  140. def load_lang(lang, apps=None):
  141. """Combine all translations from `.csv` files in all `apps`"""
  142. out = {}
  143. for app in (apps or frappe.get_all_apps(True)):
  144. path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv")
  145. if os.path.exists(path):
  146. csv_content = read_csv_file(path)
  147. try:
  148. # with file and line numbers
  149. cleaned = dict([(item[1], item[2]) for item in csv_content if item[2]])
  150. except IndexError:
  151. cleaned = dict([(item[0], item[1]) for item in csv_content if item[1]])
  152. out.update(cleaned)
  153. return out
  154. def clear_cache():
  155. """Clear all translation assets from :meth:`frappe.cache`"""
  156. cache = frappe.cache()
  157. cache.delete_value("langinfo")
  158. cache.delete_keys("lang:")
  159. cache.delete_keys("translation_assets:")
  160. def get_messages_for_app(app):
  161. """Returns all messages (list) for a specified `app`"""
  162. messages = []
  163. modules = ", ".join(['"{}"'.format(m.title().replace("_", " ")) \
  164. for m in frappe.local.app_modules[app]])
  165. # doctypes
  166. if modules:
  167. for name in frappe.db.sql_list("""select name from tabDocType
  168. where module in ({})""".format(modules)):
  169. messages.extend(get_messages_from_doctype(name))
  170. # pages
  171. for name, title in frappe.db.sql("""select name, title from tabPage
  172. where module in ({})""".format(modules)):
  173. messages.append((None, title or name))
  174. messages.extend(get_messages_from_page(name))
  175. # reports
  176. for name in frappe.db.sql_list("""select tabReport.name from tabDocType, tabReport
  177. where tabReport.ref_doctype = tabDocType.name
  178. and tabDocType.module in ({})""".format(modules)):
  179. messages.append((None, name))
  180. messages.extend(get_messages_from_report(name))
  181. for i in messages:
  182. if not isinstance(i, tuple):
  183. raise Exception
  184. # app_include_files
  185. messages.extend(get_messages_from_include_files(app))
  186. # server_messages
  187. messages.extend(get_server_messages(app))
  188. return deduplicate_messages(messages)
  189. def get_messages_from_doctype(name):
  190. """Extract all translatable messages for a doctype. Includes labels, Python code,
  191. Javascript code, html templates"""
  192. messages = []
  193. meta = frappe.get_meta(name)
  194. messages = [meta.name, meta.module]
  195. if meta.description:
  196. messages.append(meta.description)
  197. # translations of field labels, description and options
  198. for d in meta.get("fields"):
  199. messages.extend([d.label, d.description])
  200. if d.fieldtype=='Select' and d.options:
  201. options = d.options.split('\n')
  202. if not "icon" in options[0]:
  203. messages.extend(options)
  204. # translations of roles
  205. for d in meta.get("permissions"):
  206. if d.role:
  207. messages.append(d.role)
  208. messages = [message for message in messages if message]
  209. messages = [('DocType: ' + name, message) for message in messages if is_translatable(message)]
  210. # extract from js, py files
  211. doctype_file_path = frappe.get_module_path(meta.module, "doctype", meta.name, meta.name)
  212. messages.extend(get_messages_from_file(doctype_file_path + ".js"))
  213. messages.extend(get_messages_from_file(doctype_file_path + "_list.js"))
  214. messages.extend(get_messages_from_file(doctype_file_path + "_list.html"))
  215. messages.extend(get_messages_from_file(doctype_file_path + "_calendar.js"))
  216. return messages
  217. def get_messages_from_page(name):
  218. """Returns all translatable strings from a :class:`frappe.core.doctype.Page`"""
  219. return _get_messages_from_page_or_report("Page", name)
  220. def get_messages_from_report(name):
  221. """Returns all translatable strings from a :class:`frappe.core.doctype.Report`"""
  222. report = frappe.get_doc("Report", name)
  223. messages = _get_messages_from_page_or_report("Report", name,
  224. frappe.db.get_value("DocType", report.ref_doctype, "module"))
  225. # TODO position here!
  226. if report.query:
  227. messages.extend([(None, message) for message in re.findall('"([^:,^"]*):', report.query) if is_translatable(message)])
  228. messages.append((None,report.report_name))
  229. return messages
  230. def _get_messages_from_page_or_report(doctype, name, module=None):
  231. if not module:
  232. module = frappe.db.get_value(doctype, name, "module")
  233. doc_path = frappe.get_module_path(module, doctype, name)
  234. messages = get_messages_from_file(os.path.join(doc_path, name +".py"))
  235. if os.path.exists(doc_path):
  236. for filename in os.listdir(doc_path):
  237. if filename.endswith(".js") or filename.endswith(".html"):
  238. messages += get_messages_from_file(os.path.join(doc_path, filename))
  239. return messages
  240. def get_server_messages(app):
  241. """Extracts all translatable strings (tagged with :func:`frappe._`) from Python modules inside an app"""
  242. messages = []
  243. for basepath, folders, files in os.walk(frappe.get_pymodule_path(app)):
  244. for dontwalk in (".git", "public", "locale"):
  245. if dontwalk in folders: folders.remove(dontwalk)
  246. for f in files:
  247. if f.endswith(".py") or f.endswith(".html") or f.endswith(".js"):
  248. messages.extend(get_messages_from_file(os.path.join(basepath, f)))
  249. return messages
  250. def get_messages_from_include_files(app_name=None):
  251. """Extracts all translatable strings from Javascript app files"""
  252. messages = []
  253. 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 []):
  254. messages.extend(get_messages_from_file(os.path.join(frappe.local.sites_path, file)))
  255. return messages
  256. def get_messages_from_file(path):
  257. """Returns a list of transatable strings from a code file
  258. :param path: path of the code file
  259. """
  260. apps_path = get_bench_dir()
  261. if os.path.exists(path):
  262. with open(path, 'r') as sourcefile:
  263. return [(os.path.relpath(" +".join([path, str(pos)]), apps_path),
  264. message) for pos, message in extract_messages_from_code(sourcefile.read(), path.endswith(".py"))]
  265. else:
  266. return []
  267. def extract_messages_from_code(code, is_py=False):
  268. """Extracts translatable srings from a code file
  269. :param code: code from which translatable files are to be extracted
  270. :param is_py: include messages in triple quotes e.g. `_('''message''')`"""
  271. try:
  272. code = render_include(code)
  273. except TemplateError:
  274. # Exception will occur when it encounters John Resig's microtemplating code
  275. pass
  276. messages = []
  277. messages += [(m.start(), m.groups()[0]) for m in re.compile('_\("([^"]*)"').finditer(code)]
  278. messages += [(m.start(), m.groups()[0]) for m in re.compile("_\('([^']*)'").finditer(code)]
  279. if is_py:
  280. messages += [(m.start(), m.groups()[0]) for m in re.compile('_\("{3}([^"]*)"{3}.*\)').finditer(code)]
  281. messages = [(pos, message) for pos, message in messages if is_translatable(message)]
  282. return pos_to_line_no(messages, code)
  283. def is_translatable(m):
  284. if re.search("[a-z]", m) and not m.startswith("icon-") and not m.endswith("px") and not m.startswith("eval:"):
  285. return True
  286. return False
  287. def pos_to_line_no(messages, code):
  288. ret = []
  289. messages = sorted(messages, key=lambda x: x[0])
  290. newlines = [m.start() for m in re.compile('\\n').finditer(code)]
  291. line = 1
  292. newline_i = 0
  293. for pos, message in messages:
  294. while newline_i < len(newlines) and pos > newlines[newline_i]:
  295. line+=1
  296. newline_i+= 1
  297. ret.append((line, message))
  298. return ret
  299. def read_csv_file(path):
  300. """Read CSV file and return as list of list
  301. :param path: File path"""
  302. from csv import reader
  303. with codecs.open(path, 'r', 'utf-8') as msgfile:
  304. data = msgfile.read()
  305. # for japanese! #wtf
  306. data = data.replace(chr(28), "").replace(chr(29), "")
  307. data = reader([r.encode('utf-8') for r in data.splitlines()])
  308. newdata = [[unicode(val, 'utf-8') for val in row] for row in data]
  309. return newdata
  310. def write_csv_file(path, app_messages, lang_dict):
  311. """Write translation CSV file.
  312. :param path: File path, usually `[app]/translations`.
  313. :param app_messages: Translatable strings for this app.
  314. :param lang_dict: Full translated dict.
  315. """
  316. app_messages.sort()
  317. from csv import writer
  318. with open(path, 'wb') as msgfile:
  319. w = writer(msgfile, lineterminator='\n')
  320. for p, m in app_messages:
  321. t = lang_dict.get(m, '')
  322. # strip whitespaces
  323. t = re.sub('{\s?([0-9]+)\s?}', "{\g<1>}", t)
  324. w.writerow([p.encode('utf-8') if p else '', m.encode('utf-8'), t.encode('utf-8')])
  325. def get_untranslated(lang, untranslated_file, get_all=False):
  326. """Returns all untranslated strings for a language and writes in a file
  327. :param lang: Language code.
  328. :param untranslated_file: Output file path.
  329. :param get_all: Return all strings, translated or not."""
  330. clear_cache()
  331. apps = frappe.get_all_apps(True)
  332. messages = []
  333. untranslated = []
  334. for app in apps:
  335. messages.extend(get_messages_for_app(app))
  336. messages = deduplicate_messages(messages)
  337. def escape_newlines(s):
  338. return (s.replace("\\\n", "|||||")
  339. .replace("\\n", "||||")
  340. .replace("\n", "|||"))
  341. if get_all:
  342. print str(len(messages)) + " messages"
  343. with open(untranslated_file, "w") as f:
  344. for m in messages:
  345. # replace \n with ||| so that internal linebreaks don't get split
  346. f.write((escape_newlines(m[1]) + os.linesep).encode("utf-8"))
  347. else:
  348. full_dict = get_full_dict(lang)
  349. for m in messages:
  350. if not full_dict.get(m[1]):
  351. untranslated.append(m[1])
  352. if untranslated:
  353. print str(len(untranslated)) + " missing translations of " + str(len(messages))
  354. with open(untranslated_file, "w") as f:
  355. for m in untranslated:
  356. # replace \n with ||| so that internal linebreaks don't get split
  357. f.write((escape_newlines(m) + os.linesep).encode("utf-8"))
  358. else:
  359. print "all translated!"
  360. def update_translations(lang, untranslated_file, translated_file):
  361. """Update translations from a source and target file for a given language.
  362. :param lang: Language code (e.g. `en`).
  363. :param untranslated_file: File path with the messages in English.
  364. :param translated_file: File path with messages in language to be updated."""
  365. clear_cache()
  366. full_dict = get_full_dict(lang)
  367. def restore_newlines(s):
  368. return (s.replace("|||||", "\\\n")
  369. .replace("| | | | |", "\\\n")
  370. .replace("||||", "\\n")
  371. .replace("| | | |", "\\n")
  372. .replace("|||", "\n")
  373. .replace("| | |", "\n"))
  374. translation_dict = {}
  375. for key, value in zip(frappe.get_file_items(untranslated_file, ignore_empty_lines=False),
  376. frappe.get_file_items(translated_file, ignore_empty_lines=False)):
  377. # undo hack in get_untranslated
  378. translation_dict[restore_newlines(key)] = restore_newlines(value)
  379. full_dict.update(translation_dict)
  380. for app in frappe.get_all_apps(True):
  381. write_translations_file(app, lang, full_dict)
  382. def rebuild_all_translation_files():
  383. """Rebuild all translation files: `[app]/translations/[lang].csv`."""
  384. for lang in get_all_languages():
  385. for app in frappe.get_all_apps():
  386. write_translations_file(app, lang)
  387. def write_translations_file(app, lang, full_dict=None, app_messages=None):
  388. """Write a translation file for a given language.
  389. :param app: `app` for which translations are to be written.
  390. :param lang: Language code.
  391. :param full_dict: Full translated langauge dict (optional).
  392. :param app_messages: Source strings (optional).
  393. """
  394. if not app_messages:
  395. app_messages = get_messages_for_app(app)
  396. if not app_messages:
  397. return
  398. tpath = frappe.get_pymodule_path(app, "translations")
  399. frappe.create_folder(tpath)
  400. write_csv_file(os.path.join(tpath, lang + ".csv"),
  401. app_messages, full_dict or get_full_dict(lang))
  402. def send_translations(translation_dict):
  403. """Append translated dict in `frappe.local.response`"""
  404. if "__messages" not in frappe.local.response:
  405. frappe.local.response["__messages"] = {}
  406. frappe.local.response["__messages"].update(translation_dict)
  407. def deduplicate_messages(messages):
  408. ret = []
  409. op = operator.itemgetter(1)
  410. messages = sorted(messages, key=op)
  411. for k, g in itertools.groupby(messages, op):
  412. ret.append(g.next())
  413. return ret
  414. def get_bench_dir():
  415. return os.path.join(frappe.__file__, '..', '..', '..', '..')