Skip to content

Commit 6e6557f

Browse files
author
metalmon
committed
feat: localize desktop experience
- add gettext extractors for desktop icon and workspace sidebar fixtures - translate desktop launchpad, sidebar templates, and workspace blocks - refresh locale catalogs (main.pot, ru.po)
1 parent df88efe commit 6e6557f

30 files changed

Lines changed: 1036 additions & 157 deletions

babel_extractors.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
**/hooks.py,frappe.gettext.extractors.python.extract
22
**/doctype/*/*.json,frappe.gettext.extractors.doctype.extract
33
**/workspace/*/*.json,frappe.gettext.extractors.workspace.extract
4+
**/workspace_sidebar/*.json,frappe.gettext.extractors.workspace_sidebar.extract
45
**/web_form/*/*.json,frappe.gettext.extractors.web_form.extract
56
**/onboarding_step/*/*.json,frappe.gettext.extractors.onboarding_step.extract
67
**/module_onboarding/*/*.json,frappe.gettext.extractors.module_onboarding.extract
78
**/report/*/*.json,frappe.gettext.extractors.report.extract
9+
**/desktop_icon/*.json,frappe.gettext.extractors.desktop_icon.extract
810
**.py,frappe.gettext.extractors.python.extract
911
**/templates/**.js,frappe.gettext.extractors.html_template.extract
1012
**.js,frappe.gettext.extractors.javascript.extract

frappe/boot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import frappe
1010
import frappe.defaults
1111
import frappe.desk.desk_page
12+
from frappe import _
1213
from frappe.core.doctype.installed_applications.installed_applications import (
1314
get_setup_wizard_completed_apps,
1415
)

frappe/desk/page/desktop/desktop.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
type="text"
1515
class="form-control"
1616
aria-haspopup="true"
17-
placeholder="Search for type a command"
17+
placeholder="{{ __("Search for type a command") }}"
1818
>
1919
<span class="desktop-search-icon">
2020
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>

frappe/desk/page/desktop/desktop.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ function get_workspaces_from_app_name(app_name) {
3434
if (app.length > 0) return app[0].workspaces;
3535
}
3636

37+
function get_workspaces_plural_text(count) {
38+
// Pluralization rule: n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2
39+
// 0: 1, 21, 31... (singular)
40+
// 1: 2-4, 22-24... (few)
41+
// 2: 5-20, 25-30... (many)
42+
let plural_form = 2; // default
43+
if (count % 10 == 1 && count % 100 != 11) {
44+
plural_form = 0;
45+
} else if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) {
46+
plural_form = 1;
47+
}
48+
49+
// Use context for plural forms
50+
if (plural_form === 0) {
51+
return __("Workspace", null, "plural_single");
52+
}
53+
if (plural_form === 1) {
54+
return __("Workspaces", null, "plural_few");
55+
}
56+
return __("Workspaces", null, "plural_many");
57+
}
58+
3759
function get_route(desktop_icon) {
3860
let route;
3961
if (!desktop_icon) return;
@@ -82,7 +104,7 @@ function save_desktop() {
82104
`${frappe.session.user}:desktop`,
83105
JSON.stringify(frappe.boot.desktop_icons)
84106
);
85-
frappe.toast("Desktop Saved");
107+
frappe.toast(__("Desktop Saved"));
86108
frappe.pages["desktop"].desktop_page.update();
87109
}
88110

@@ -166,25 +188,25 @@ class DesktopPage {
166188
let menu_items = [
167189
{
168190
icon: "edit",
169-
label: "Edit Profile",
191+
label: __("Edit Profile"),
170192
url: `/update-profile/${frappe.session.user}`,
171193
},
172194
{
173195
icon: "lock",
174-
label: "Reset Password",
196+
label: __("Reset Password"),
175197
url: "/update-password",
176198
},
177199
{
178200
icon: "rotate-ccw",
179-
label: "Reset to Default",
201+
label: __("Reset to Default"),
180202
onClick: function () {
181203
reset_to_default();
182204
window.location.reload();
183205
},
184206
},
185207
{
186208
icon: "log-out",
187-
label: "Logout",
209+
label: __("Logout"),
188210
onClick: function () {
189211
frappe.app.logout();
190212
},
@@ -447,7 +469,8 @@ class DesktopIconGrid {
447469
pull: true,
448470
},
449471
setData: function (/** DataTransfer */ dataTransfer, /** HTMLElement*/ dragEl) {
450-
let title = $(dragEl).find(".icon-title").text();
472+
let titleElement = $(dragEl).find(".icon-title");
473+
let title = titleElement.attr("data-original-label") || titleElement.text();
451474
let icon = me.icons.find((d) => {
452475
return d.icon_title === title;
453476
});
@@ -465,7 +488,8 @@ class DesktopIconGrid {
465488
} else {
466489
let from = $(evt.from.parentElement);
467490
let to = $(evt.to.parentElement);
468-
let title = $(evt.item).find(".icon-title").text();
491+
let titleElement = $(evt.item).find(".icon-title");
492+
let title = titleElement.attr("data-original-label") || titleElement.text();
469493
let selected_icon = get_desktop_icon_by_label(title);
470494
if ($(to.get(0).parentElement)) {
471495
me.reorder_icons(me.sortable.toArray());
@@ -477,7 +501,7 @@ class DesktopIconGrid {
477501
}
478502
}
479503
} else {
480-
frappe.toast("Nothing changed");
504+
frappe.toast(__("Nothing changed"));
481505
}
482506
save_desktop();
483507
},
@@ -553,8 +577,10 @@ class DesktopIcon {
553577
modal.show();
554578
});
555579
if (this.icon_type == "App") {
580+
let count = this.child_icons.length;
581+
let plural_text = get_workspaces_plural_text(count);
556582
$($(this.icon_caption_area).children()[1]).html(
557-
`${this.child_icons.length} Workspaces`
583+
`${count} ${plural_text}`
558584
);
559585
}
560586
} else {
@@ -566,7 +592,7 @@ class DesktopIcon {
566592
if (me.icon_data.sidebar == "My Workspaces") {
567593
let sidebar_name = me.icon_data.sidebar.toLowerCase();
568594
if (frappe.boot.workspace_sidebar_item[sidebar_name].items.length == 0) {
569-
frappe.toast("No Private Workspaces for user");
595+
frappe.toast(__("No Private Workspaces for user"));
570596
} else {
571597
let workspace_name =
572598
frappe.boot.workspace_sidebar_item[sidebar_name].items[0]["link_to"];
@@ -648,15 +674,16 @@ class DesktopModal {
648674
});
649675
}
650676
make_modal(icon_title) {
677+
const translated_title = __(icon_title);
651678
if ($(".desktop-modal").length == 0) {
652-
this.modal = new frappe.get_modal(icon_title, "");
679+
this.modal = new frappe.get_modal(translated_title, "");
653680
this.modal.find(".modal-header").addClass("desktop-modal-heading");
654681
this.modal.addClass("desktop-modal");
655682
this.modal.find(".modal-dialog").attr("id", "desktop-modal");
656683
this.modal.find(".modal-body").addClass("desktop-modal-body");
657684
this.$child_icons_wrapper = this.modal.find(".desktop-modal-body");
658685
} else {
659-
this.modal.find(".modal-title").text(icon_title);
686+
this.modal.find(".modal-title").text(translated_title);
660687
$(this.modal.find(".modal-body")).empty();
661688
if (frappe.desktop_utils.modal_stack.length == 1) {
662689
this.title_section.find(".icon").remove();

frappe/desk/page/desktop/desktop.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ def get_context(context):
1313
context.brand_logo = brand_logo
1414
context.desktop_icons = get_desktop_icons()
1515
context.current_user = frappe.session.user
16+
context["__"] = frappe._
1617
return context
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import orjson
2+
3+
4+
def extract(fileobj, *args, **kwargs):
5+
"""Extract labels from Desktop Icon JSON fixtures."""
6+
data = orjson.loads(fileobj.read())
7+
8+
if isinstance(data, list):
9+
return
10+
11+
if data.get("doctype") != "Desktop Icon":
12+
return
13+
14+
label = data.get("label")
15+
if label:
16+
context = data.get("parent_icon") or data.get("app") or "Desktop Icon"
17+
yield None, "_", label, [f"Desktop Icon label (parent: {context})"]
18+

frappe/gettext/extractors/workspace.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import json
1+
import orjson
22

33

44
def extract(fileobj, *args, **kwargs):
@@ -7,7 +7,7 @@ def extract(fileobj, *args, **kwargs):
77
:param fileobj: the file-like object the messages should be extracted from
88
:rtype: `iterator`
99
"""
10-
data = json.load(fileobj)
10+
data = orjson.loads(fileobj.read())
1111

1212
if isinstance(data, list):
1313
return
@@ -79,7 +79,12 @@ def extract(fileobj, *args, **kwargs):
7979
if quick_list.get("label")
8080
)
8181

82-
content = json.loads(data.get("content", "[]"))
82+
content_raw = data.get("content")
83+
try:
84+
content = orjson.loads(content_raw) if content_raw else []
85+
except orjson.JSONDecodeError:
86+
content = []
87+
8388
for item in content:
8489
item_type = item.get("type")
8590
if item_type in ("header", "paragraph"):
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import orjson
2+
3+
4+
def extract(fileobj, *args, **kwargs):
5+
"""Extract labels from Workspace Sidebar JSON fixtures."""
6+
data = orjson.loads(fileobj.read())
7+
8+
if isinstance(data, list):
9+
return
10+
11+
if data.get("doctype") != "Workspace Sidebar":
12+
return
13+
14+
title = data.get("title") or data.get("name")
15+
if title:
16+
yield None, "_", title, ["Workspace Sidebar title"]
17+
18+
for item in data.get("items", []):
19+
label = item.get("label")
20+
if not label:
21+
continue
22+
23+
item_type = item.get("type") or "Item"
24+
yield (
25+
None,
26+
"_",
27+
label,
28+
[f"Workspace Sidebar {item_type} label under {title or 'Sidebar'}"],
29+
)
30+

0 commit comments

Comments
 (0)