🎉 Add new translations management script

This commit is contained in:
Andrey Antukh 2024-06-13 11:45:01 +02:00 committed by AzazelN28
parent 62cea62356
commit 139dd7d80f
22 changed files with 677 additions and 308 deletions

View file

@ -55,6 +55,7 @@
"draft-js": "git+https://github.com/penpot/draft-js.git#commit=4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0", "draft-js": "git+https://github.com/penpot/draft-js.git#commit=4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
"express": "^4.19.2", "express": "^4.19.2",
"fancy-log": "^2.0.0", "fancy-log": "^2.0.0",
"getopts": "^2.3.0",
"gettext-parser": "^8.0.0", "gettext-parser": "^8.0.0",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",

377
frontend/scripts/translations.js Executable file
View file

@ -0,0 +1,377 @@
#!/usr/bin/env node
import getopts from "getopts";
import { promises as fs, createReadStream } from "fs";
import gt from "gettext-parser";
import l from "lodash";
import path from "path";
import readline from "readline";
const baseLocale = "en";
async function* getFiles(dir) {
// console.log("getFiles", dir)
const dirents = await fs.readdir(dir, { withFileTypes: true });
for (const dirent of dirents) {
let res = path.resolve(dir, dirent.name);
res = path.relative(".", res);
if (dirent.isDirectory()) {
yield* getFiles(res);
} else {
yield res;
}
}
}
async function translationExists(locale) {
const target = path.normalize("./translations/");
const targetPath = path.join(target, `${locale}.po`);
try {
const result = await fs.stat(targetPath);
return true;
} catch (cause) {
return false;
}
}
async function readLocaleByPath(path) {
const content = await fs.readFile(path);
return gt.po.parse(content, "utf-8");
}
async function writeLocaleByPath(path, data) {
const buff = gt.po.compile(data, { sort: true });
await fs.writeFile(path, buff);
}
async function readLocale(locale) {
const target = path.normalize("./translations/");
const targetPath = path.join(target, `${locale}.po`);
return readLocaleByPath(targetPath);
}
async function writeLocale(locale, data) {
const target = path.normalize("./translations/");
const targetPath = path.join(target, `${locale}.po`);
return writeLocaleByPath(targetPath, data);
}
async function* scanLocales() {
const fileRe = /.+\.po$/;
const target = path.normalize("./translations/");
const parent = path.join(target, "..");
for await (const f of getFiles(target)) {
if (!fileRe.test(f)) continue;
const data = path.parse(f);
yield data;
}
}
async function processLocale(options, f) {
let locales = options.locale;
if (typeof locales === "string") {
locales = locales.split(/,/);
} else if (Array.isArray(locales)) {
} else if (locales === undefined) {
} else {
console.error(`Invalid value found on locales parameter: '${locales}'`);
process.exit(-1);
}
for await (const { name } of scanLocales()) {
if (locales === undefined || locales.includes(name)) {
await f(name);
}
}
}
async function processTranslation(data, prefix, f) {
for (let key of Object.keys(data.translations[""])) {
if (key === prefix || key.startsWith(prefix)) {
let value = data.translations[""][key];
value = await f(value);
data.translations[""][key] = value;
}
}
return data;
}
async function* readLines(filePath) {
const fileStream = createReadStream(filePath);
const reader = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
let counter = 1;
for await (const line of reader) {
yield [counter, line];
counter++;
}
}
const trRe1 = /\(tr\s+"([\w\.\-]+)"/g;
function getTranslationStrings(line) {
const result = Array.from(line.matchAll(trRe1)).map((match) => {
return match[1];
});
return result;
}
async function deleteByPrefix(options, prefix, ...params) {
if (!prefix) {
console.error(`Prefix undefined`);
process.exit(1);
}
await processLocale(options, async (locale) => {
const data = await readLocale(locale);
let deleted = [];
for (const [key, value] of Object.entries(data.translations[""])) {
if (key.startsWith(prefix)) {
delete data.translations[""][key];
deleted.push(key);
}
}
await writeLocale(locale, data);
console.log(
`=> Processed locale '${locale}': deleting prefix '${prefix}' (deleted=${deleted.length})`,
);
if (options.verbose) {
for (let key of deleted) {
console.log(`-> Deleted key: ${key}`);
}
}
});
}
async function markFuzzy(options, prefix, ...other) {
if (!prefix) {
console.error(`Prefix undefined`);
process.exit(1);
}
await processLocale(options, async (locale) => {
let data = await readLocale(locale);
data = await processTranslation(data, prefix, (translation) => {
if (translation.comments === undefined) {
translation.comments = {};
}
const flagData = translation.comments.flag ?? "";
const flags = flagData.split(/\s*,\s*/).filter((s) => s !== "");
if (!flags.includes("fuzzy")) {
flags.push("fuzzy");
}
translation.comments.flag = flags.join(", ");
console.log(
`=> Processed '${locale}': marking fuzzy '${translation.msgid}'`,
);
return translation;
});
await writeLocale(locale, data);
});
}
async function rehash(options, ...other) {
const fileRe = /.+\.(?:clj|cljs|cljc)$/;
// Iteration 1: process all locales and update it with existing
// entries on the source code.
const used = await (async function () {
const result = {};
for await (const f of getFiles("src")) {
if (!fileRe.test(f)) continue;
for await (const [n, line] of readLines(f)) {
const strings = getTranslationStrings(line);
strings.forEach((key) => {
const entry = `${f}:${n}`;
if (result[key] !== undefined) {
result[key].push(entry);
} else {
result[key] = [entry];
}
});
}
}
await processLocale({ locale: baseLocale }, async (locale) => {
const data = await readLocale(locale);
for (let [key, val] of Object.entries(result)) {
let entry = data.translations[""][key];
if (entry === undefined) {
entry = {
msgid: key,
comments: {
reference: val.join(", "),
flag: "fuzzy",
},
msgstr: [""],
};
} else {
if (entry.comments === undefined) {
entry.comments = {};
}
entry.comments.reference = val.join(", ");
const flagData = entry.comments.flag ?? "";
const flags = flagData.split(/\s*,\s*/).filter((s) => s !== "");
if (flags.includes("unused")) {
flags = flags.filter((o) => o !== "unused");
}
entry.comments.flag = flags.join(", ");
}
data.translations[""][key] = entry;
}
await writeLocale(locale, data);
const keys = Object.keys(data.translations[""]);
console.log(`=> Found ${keys.length} used translations`);
});
return result;
})();
// Iteration 2: process only base locale and properly detect unused
// translation strings.
await (async function () {
let totalUnused = 0;
await processLocale({ locale: baseLocale }, async (locale) => {
const data = await readLocale(locale);
for (let [key, val] of Object.entries(data.translations[""])) {
if (key === "") continue;
if (!used.hasOwnProperty(key)) {
totalUnused++;
const entry = data.translations[""][key];
if (entry.comments === undefined) {
entry.comments = {};
}
const flagData = entry.comments.flag ?? "";
const flags = flagData.split(/\s*,\s*/).filter((s) => s !== "");
if (!flags.includes("unused")) {
flags.push("unused");
}
entry.comments.flag = flags.join(", ");
data.translations[""][key] = entry;
}
}
await writeLocale(locale, data);
});
console.log(`=> Found ${totalUnused} unused strings`);
})();
}
async function synchronize(options, ...other) {
const baseData = await readLocale(baseLocale);
await processLocale(options, async (locale) => {
if (locale === baseLocale) return;
const data = await readLocale(locale);
for (let [key, val] of Object.entries(baseData.translations[""])) {
if (key === "") continue;
const baseEntry = baseData.translations[""][key];
const entry = data.translations[""][key];
if (entry === undefined) {
// Do nothing
} else {
entry.comments = baseEntry.comments;
data.translations[""][key] = entry;
}
}
for (let [key, val] of Object.entries(data.translations[""])) {
if (key === "") continue;
const baseEntry = baseData.translations[""][key];
const entry = data.translations[""][key];
if (baseEntry === undefined) {
delete data.translations[""][key];
}
}
await writeLocale(locale, data);
});
}
const options = getopts(process.argv.slice(2), {
boolean: ["h", "v"],
alias: {
help: ["h"],
locale: ["l"],
verbose: ["v"],
},
stopEarly: true,
});
const [command, ...params] = options._;
if (command === "rehash") {
await rehash(options, ...params);
} else if (command === "sync") {
await synchronize(options, ...params);
} else if (command === "delete") {
await deleteByPrefix(options, ...params);
} else if (command === "fuzzy") {
await markFuzzy(options, ...params);
} else {
console.log(`Translations manipulation script.
How to use:
./scripts/translation.js <options> <subcommand>
Available options:
--locale -l : specify a concrete locale
--verbose -v : enables verbose output
--help -h : prints this help
Available subcommands:
rehash : reads and writes all translations files, sorting and validating
sync : synchronize baselocale file with all other locale files
delete <prefix> : delete all entries that matches the prefix
fuzzy <prefix> : mark as fuzzy all entries that matches the prefix
`);
}

View file

@ -1,31 +0,0 @@
import { promises as fs } from "fs";
import gt from "gettext-parser";
import l from "lodash";
import path from "path";
async function* getFiles(dir) {
const dirents = await fs.readdir(dir, { withFileTypes: true });
for (const dirent of dirents) {
const res = path.resolve(dir, dirent.name);
if (dirent.isDirectory()) {
yield* getFiles(res);
} else {
yield res;
}
}
}
(async () => {
const fileRe = /.+\.po$/;
const target = path.normalize("./translations/");
const parent = path.join(target, "..");
for await (const f of getFiles(target)) {
if (!fileRe.test(f)) continue;
const entry = path.relative(parent, f);
console.log(`=> processing: ${entry}`);
const content = await fs.readFile(f);
const data = gt.po.parse(content, "utf-8");
const buff = gt.po.compile(data, { sort: true });
await fs.writeFile(f, buff);
}
})();

View file

@ -18,7 +18,7 @@
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.main.ui.components.link :as lk] [app.main.ui.components.link :as lk]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.i18n :refer [tr tr-html]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.storage :as sto] [app.util.storage :as sto]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
@ -197,10 +197,11 @@
[] []
(let [terms-label (let [terms-label
(mf/html (mf/html
[:& tr-html [:> i18n/tr-html*
{:tag-name "div" {:tag-name "div"
:label "auth.terms-and-privacy-agreement" :content (tr "auth.terms-and-privacy-agreement"
:params [cf/terms-of-service-uri cf/privacy-policy-uri]}])] cf/terms-of-service-uri
cf/privacy-policy-uri)}])]
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)} [:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy [:& fm/input {:name :accept-terms-and-privacy

View file

@ -11,7 +11,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as k] [app.util.keyboard :as k]
[goog.events :as events] [goog.events :as events]
[rumext.v2 :as mf]) [rumext.v2 :as mf])
@ -30,15 +30,13 @@
cancel-label cancel-label
accept-label accept-label
accept-style] :as props}] accept-style] :as props}]
(let [locale (mf/deref i18n/locale) (let [on-accept (or on-accept identity)
on-accept (or on-accept identity)
on-cancel (or on-cancel identity) on-cancel (or on-cancel identity)
message (or message (t locale "ds.confirm-title")) message (or message (tr "ds.confirm-title"))
cancel-label (or cancel-label (tr "ds.confirm-cancel")) cancel-label (or cancel-label (tr "ds.confirm-cancel"))
accept-label (or accept-label (tr "ds.confirm-ok")) accept-label (or accept-label (tr "ds.confirm-ok"))
accept-style (or accept-style :danger) accept-style (or accept-style :danger)
title (or title (t locale "ds.confirm-title")) title (or title (tr "ds.confirm-title"))
accept-fn accept-fn
(mf/use-callback (mf/use-callback

View file

@ -167,7 +167,7 @@
[:div {:class (stl/css :dashboard-fonts-hero)} [:div {:class (stl/css :dashboard-fonts-hero)}
[:div {:class (stl/css :desc)} [:div {:class (stl/css :desc)}
[:h2 (tr "labels.upload-custom-fonts")] [:h2 (tr "labels.upload-custom-fonts")]
[:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}] [:> i18n/tr-html* {:content (tr "dashboard.fonts.hero-text1")}]
[:button {:class (stl/css :btn-primary) [:button {:class (stl/css :btn-primary)
:on-click on-click :on-click on-click

View file

@ -12,7 +12,7 @@
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc empty-placeholder (mf/defc empty-placeholder
[{:keys [dragging? limit origin create-fn] :as props}] [{:keys [dragging? limit origin create-fn]}]
(let [on-click (let [on-click
(mf/use-fn (mf/use-fn
(mf/deps create-fn) (mf/deps create-fn)
@ -29,7 +29,7 @@
[:div {:class (stl/css :grid-empty-placeholder :libs) [:div {:class (stl/css :grid-empty-placeholder :libs)
:data-testid "empty-placeholder"} :data-testid "empty-placeholder"}
[:div {:class (stl/css :text)} [:div {:class (stl/css :text)}
[:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]] [:> i18n/tr-html* {:content (tr "dashboard.empty-placeholder-drafts")}]]]
:else :else
[:div [:div

View file

@ -693,8 +693,8 @@
[:div {:class (stl/css :empty-invitations)} [:div {:class (stl/css :empty-invitations)}
[:span (tr "labels.no-invitations")] [:span (tr "labels.no-invitations")]
(when can-invite? (when can-invite?
[:& i18n/tr-html {:label "labels.no-invitations-hint" [:> i18n/tr-html* {:content (tr "labels.no-invitations-hint")
:tag-name "span"}])]) :tag-name "span"}])])
(mf/defc invitation-section (mf/defc invitation-section
[{:keys [team invitations] :as props}] [{:keys [team invitations] :as props}]
@ -878,8 +878,8 @@
[:div {:class (stl/css :webhooks-hero-container)} [:div {:class (stl/css :webhooks-hero-container)}
[:h2 {:class (stl/css :hero-title)} [:h2 {:class (stl/css :hero-title)}
(tr "labels.webhooks")] (tr "labels.webhooks")]
[:& i18n/tr-html {:class (stl/css :hero-desc) [:> i18n/tr-html* {:class (stl/css :hero-desc)
:label "dashboard.webhooks.description"}] :content (tr "dashboard.webhooks.description")}]
[:button {:class (stl/css :hero-btn) [:button {:class (stl/css :hero-btn)
:on-click #(st/emit! (modal/show :webhook {}))} :on-click #(st/emit! (modal/show :webhook {}))}
(tr "dashboard.webhooks.create")]]) (tr "dashboard.webhooks.create")]])

View file

@ -432,12 +432,12 @@
[:label {:for (str "export-" type) [:label {:for (str "export-" type)
:class (stl/css-case :global/checked (= selected type))} :class (stl/css-case :global/checked (= selected type))}
;; Execution time translation strings: ;; Execution time translation strings:
;; dashboard.export.options.all.message ;; (tr "dashboard.export.options.all.message")
;; dashboard.export.options.all.title ;; (tr "dashboard.export.options.all.title")
;; dashboard.export.options.detach.message ;; (tr "dashboard.export.options.detach.message")
;; dashboard.export.options.detach.title ;; (tr "dashboard.export.options.detach.title")
;; dashboard.export.options.merge.message ;; (tr "dashboard.export.options.merge.message")
;; dashboard.export.options.merge.title ;; (tr "dashboard.export.options.merge.title")
[:span {:class (stl/css-case :global/checked (= selected type))} [:span {:class (stl/css-case :global/checked (= selected type))}
(when (= selected type) (when (= selected type)
i/status-tick)] i/status-tick)]

View file

@ -33,18 +33,16 @@
(mf/defc settings (mf/defc settings
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [section (get-in route [:data :name]) (let [section (get-in route [:data :name])
profile (mf/deref refs/profile) profile (mf/deref refs/profile)]
locale (mf/deref i18n/locale)]
(hooks/use-shortcuts ::dashboard sc/shortcuts) (hooks/use-shortcuts ::dashboard sc/shortcuts)
(mf/use-effect (mf/with-effect [profile]
#(when (nil? profile) (when (nil? profile)
(st/emit! (rt/nav :auth-login)))) (st/emit! (rt/nav :auth-login))))
[:section {:class (stl/css :dashboard-layout-refactor :dashboard)} [:section {:class (stl/css :dashboard-layout-refactor :dashboard)}
[:& sidebar {:profile profile [:& sidebar {:profile profile
:locale locale
:section section}] :section section}]
[:div {:class (stl/css :dashboard-content)} [:div {:class (stl/css :dashboard-content)}
@ -52,16 +50,16 @@
[:section {:class (stl/css :dashboard-container)} [:section {:class (stl/css :dashboard-container)}
(case section (case section
:settings-profile :settings-profile
[:& profile-page {:locale locale}] [:& profile-page]
:settings-feedback :settings-feedback
[:& feedback-page] [:& feedback-page]
:settings-password :settings-password
[:& password-page {:locale locale}] [:& password-page]
:settings-options :settings-options
[:& options-page {:locale locale}] [:& options-page]
:settings-access-tokens :settings-access-tokens
[:& access-tokens-page])]]])) [:& access-tokens-page])]]]))

View file

@ -13,7 +13,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [tr]]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -69,7 +69,7 @@
::password-old])) ::password-old]))
(mf/defc password-form (mf/defc password-form
[{:keys [locale] :as props}] []
(let [initial (mf/use-memo (constantly {:password-old nil})) (let [initial (mf/use-memo (constantly {:password-old nil}))
form (fm/use-form :spec ::password-form form (fm/use-form :spec ::password-form
:validators [(fm/validate-not-all-spaces :password-old (tr "auth.password-not-empty")) :validators [(fm/validate-not-all-spaces :password-old (tr "auth.password-not-empty"))
@ -86,35 +86,35 @@
{:type "password" {:type "password"
:name :password-old :name :password-old
:auto-focus? true :auto-focus? true
:label (t locale "labels.old-password")}]] :label (tr "labels.old-password")}]]
[:div {:class (stl/css :fields-row)} [:div {:class (stl/css :fields-row)}
[:& fm/input [:& fm/input
{:type "password" {:type "password"
:name :password-1 :name :password-1
:show-success? true :show-success? true
:label (t locale "labels.new-password")}]] :label (tr "labels.new-password")}]]
[:div {:class (stl/css :fields-row)} [:div {:class (stl/css :fields-row)}
[:& fm/input [:& fm/input
{:type "password" {:type "password"
:name :password-2 :name :password-2
:show-success? true :show-success? true
:label (t locale "labels.confirm-password")}]] :label (tr "labels.confirm-password")}]]
[:> fm/submit-button* [:> fm/submit-button*
{:label (t locale "dashboard.password-change") {:label (tr "dashboard.password-change")
:data-testid "submit-password" :data-testid "submit-password"
:class (stl/css :update-btn)}]])) :class (stl/css :update-btn)}]]))
;; --- Password Page ;; --- Password Page
(mf/defc password-page (mf/defc password-page
[{:keys [locale]}] []
(mf/use-effect (mf/with-effect []
#(dom/set-html-title (tr "title.settings.password"))) (dom/set-html-title (tr "title.settings.password")))
[:section {:class (stl/css :dashboard-settings)} [:section {:class (stl/css :dashboard-settings)}
[:div {:class (stl/css :form-container)} [:div {:class (stl/css :form-container)}
[:h2 (t locale "dashboard.password-change")] [:h2 (tr "dashboard.password-change")]
[:& password-form {:locale locale}]]]) [:& password-form]]])

View file

@ -115,10 +115,9 @@
(mf/defc sidebar (mf/defc sidebar
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
::mf/props :obj} ::mf/props :obj}
[{:keys [profile locale section]}] [{:keys [profile section]}]
[:div {:class (stl/css :dashboard-sidebar :settings)} [:div {:class (stl/css :dashboard-sidebar :settings)}
[:& sidebar-content {:profile profile [:& sidebar-content {:profile profile
:section section}] :section section}]
[:& profile-section {:profile profile [:& profile-section {:profile profile}]])
:locale locale}]])

View file

@ -142,9 +142,9 @@
[:div {:class (stl/css :global/attr-label)} [:div {:class (stl/css :global/attr-label)}
(tr "inspect.attributes.typography.text-decoration")] (tr "inspect.attributes.typography.text-decoration")]
;; Execution time translation strings: ;; Execution time translation strings:
;; inspect.attributes.typography.text-decoration.none ;; (tr "inspect.attributes.typography.text-decoration.none")
;; inspect.attributes.typography.text-decoration.strikethrough ;; (tr "inspect.attributes.typography.text-decoration.strikethrough")
;; inspect.attributes.typography.text-decoration.underline ;; (tr "inspect.attributes.typography.text-decoration.underline")
[:div {:class (stl/css :global/attr-value)} [:div {:class (stl/css :global/attr-value)}
[:& copy-button {:data (copy-style-data style :text-decoration)} [:& copy-button {:data (copy-style-data style :text-decoration)}
[:div {:class (stl/css :button-children)} [:div {:class (stl/css :button-children)}
@ -155,11 +155,11 @@
[:div {:class (stl/css :global/attr-label)} [:div {:class (stl/css :global/attr-label)}
(tr "inspect.attributes.typography.text-transform")] (tr "inspect.attributes.typography.text-transform")]
;; Execution time translation strings: ;; Execution time translation strings:
;; inspect.attributes.typography.text-transform.lowercase ;; (tr "inspect.attributes.typography.text-transform.lowercase")
;; inspect.attributes.typography.text-transform.none ;; (tr "inspect.attributes.typography.text-transform.none")
;; inspect.attributes.typography.text-transform.titlecase ;; (tr "inspect.attributes.typography.text-transform.titlecase")
;; inspect.attributes.typography.text-transform.uppercase ;; (tr "inspect.attributes.typography.text-transform.uppercase")
;; inspect.attributes.typography.text-transform.unset ;; (tr "inspect.attributes.typography.text-transform.unset")
[:div {:class (stl/css :global/attr-value)} [:div {:class (stl/css :global/attr-value)}
[:& copy-button {:data (copy-style-data style :text-transform)} [:& copy-button {:data (copy-style-data style :text-transform)}
[:div {:class (stl/css :button-children)} [:div {:class (stl/css :button-children)}

View file

@ -94,18 +94,18 @@
[:* [:*
[:span {:class (stl/css :shape-icon)} [:span {:class (stl/css :shape-icon)}
[:& sir/element-icon {:shape first-shape :main-instance? main-instance?}]] [:& sir/element-icon {:shape first-shape :main-instance? main-instance?}]]
;; Execution time translation strings: ;; Execution time translation strings:
;; inspect.tabs.code.selected.circle ;; (tr "inspect.tabs.code.selected.circle")
;; inspect.tabs.code.selected.component ;; (tr "inspect.tabs.code.selected.component")
;; inspect.tabs.code.selected.curve ;; (tr "inspect.tabs.code.selected.curve")
;; inspect.tabs.code.selected.frame ;; (tr "inspect.tabs.code.selected.frame")
;; inspect.tabs.code.selected.group ;; (tr "inspect.tabs.code.selected.group")
;; inspect.tabs.code.selected.image ;; (tr "inspect.tabs.code.selected.image")
;; inspect.tabs.code.selected.mask ;; (tr "inspect.tabs.code.selected.mask")
;; inspect.tabs.code.selected.path ;; (tr "inspect.tabs.code.selected.path")
;; inspect.tabs.code.selected.rect ;; (tr "inspect.tabs.code.selected.rect")
;; inspect.tabs.code.selected.svg-raw ;; (tr "inspect.tabs.code.selected.svg-raw")
;; inspect.tabs.code.selected.text ;; (tr "inspect.tabs.code.selected.text")
[:span {:class (stl/css :layer-title)} (:name first-shape)]])] [:span {:class (stl/css :layer-title)} (:name first-shape)]])]
[:div {:class (stl/css :inspect-content)} [:div {:class (stl/css :inspect-content)}
[:& tab-container {:on-change-tab handle-change-tab [:& tab-container {:on-change-tab handle-change-tab

View file

@ -474,7 +474,7 @@
[:& menu-separator] [:& menu-separator]
(for [entry components-menu-entries :when (not (nil? entry))] (for [entry components-menu-entries :when (not (nil? entry))]
[:& menu-entry {:key (uuid/next) [:& menu-entry {:key (uuid/next)
:title (tr (:msg entry)) :title (:title entry)
:shortcut (when (contains? entry :shortcut) (sc/get-tooltip (:shortcut entry))) :shortcut (when (contains? entry :shortcut) (sc/get-tooltip (:shortcut entry)))
:on-click (:action entry)}])])])) :on-click (:action entry)}])])]))

View file

@ -421,27 +421,27 @@
(ts/schedule 1000 do-show-component))) (ts/schedule 1000 do-show-component)))
menu-entries [(when (and (not multi) main-instance?) menu-entries [(when (and (not multi) main-instance?)
{:msg "workspace.shape.menu.show-in-assets" {:title (tr "workspace.shape.menu.show-in-assets")
:action do-show-in-assets}) :action do-show-in-assets})
(when (and (not multi) main-instance? local-component? lacks-annotation? components-v2) (when (and (not multi) main-instance? local-component? lacks-annotation? components-v2)
{:msg "workspace.shape.menu.create-annotation" {:title (tr "workspace.shape.menu.create-annotation")
:action do-create-annotation}) :action do-create-annotation})
(when can-detach? (when can-detach?
{:msg (if (> (count copies) 1) {:title (if (> (count copies) 1)
"workspace.shape.menu.detach-instances-in-bulk" (tr "workspace.shape.menu.detach-instances-in-bulk")
"workspace.shape.menu.detach-instance") (tr "workspace.shape.menu.detach-instance"))
:action do-detach-component :action do-detach-component
:shortcut :detach-component}) :shortcut :detach-component})
(when can-reset-overrides? (when can-reset-overrides?
{:msg "workspace.shape.menu.reset-overrides" {:title (tr "workspace.shape.menu.reset-overrides")
:action do-reset-component}) :action do-reset-component})
(when (and (seq restorable-copies) components-v2) (when (and (seq restorable-copies) components-v2)
{:msg "workspace.shape.menu.restore-main" {:title (tr "workspace.shape.menu.restore-main")
:action do-restore-component}) :action do-restore-component})
(when can-show-component? (when can-show-component?
{:msg "workspace.shape.menu.show-main" {:title (tr "workspace.shape.menu.show-main")
:action do-show-component}) :action do-show-component})
(when can-update-main? (when can-update-main?
{:msg "workspace.shape.menu.update-main" {:title (tr "workspace.shape.menu.update-main")
:action do-update-component})]] :action do-update-component})]]
(filter (complement nil?) menu-entries))) (filter (complement nil?) menu-entries)))

View file

@ -16,7 +16,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [t] :as i18n] [app.util.i18n :refer [tr] :as i18n]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -104,49 +104,49 @@
(defn entry-type->message (defn entry-type->message
"Formats the message that will be displayed to the user" "Formats the message that will be displayed to the user"
[locale type multiple?] [type multiple?]
(let [arity (if multiple? "multiple" "single") (let [arity (if multiple? "multiple" "single")
attribute (name (or type :multiple))] attribute (name (or type :multiple))]
;; Execution time translation strings: ;; Execution time translation strings:
;; workspace.undo.entry.multiple.circle ;; (tr "workspace.undo.entry.multiple.circle")
;; workspace.undo.entry.multiple.color ;; (tr "workspace.undo.entry.multiple.color")
;; workspace.undo.entry.multiple.component ;; (tr "workspace.undo.entry.multiple.component")
;; workspace.undo.entry.multiple.curve ;; (tr "workspace.undo.entry.multiple.curve")
;; workspace.undo.entry.multiple.frame ;; (tr "workspace.undo.entry.multiple.frame")
;; workspace.undo.entry.multiple.group ;; (tr "workspace.undo.entry.multiple.group")
;; workspace.undo.entry.multiple.media ;; (tr "workspace.undo.entry.multiple.media")
;; workspace.undo.entry.multiple.multiple ;; (tr "workspace.undo.entry.multiple.multiple")
;; workspace.undo.entry.multiple.page ;; (tr "workspace.undo.entry.multiple.page")
;; workspace.undo.entry.multiple.path ;; (tr "workspace.undo.entry.multiple.path")
;; workspace.undo.entry.multiple.rect ;; (tr "workspace.undo.entry.multiple.rect")
;; workspace.undo.entry.multiple.shape ;; (tr "workspace.undo.entry.multiple.shape")
;; workspace.undo.entry.multiple.text ;; (tr "workspace.undo.entry.multiple.text")
;; workspace.undo.entry.multiple.typography ;; (tr "workspace.undo.entry.multiple.typography")
;; workspace.undo.entry.single.circle ;; (tr "workspace.undo.entry.single.circle")
;; workspace.undo.entry.single.color ;; (tr "workspace.undo.entry.single.color")
;; workspace.undo.entry.single.component ;; (tr "workspace.undo.entry.single.component")
;; workspace.undo.entry.single.curve ;; (tr "workspace.undo.entry.single.curve")
;; workspace.undo.entry.single.frame ;; (tr "workspace.undo.entry.single.frame")
;; workspace.undo.entry.single.group ;; (tr "workspace.undo.entry.single.group")
;; workspace.undo.entry.single.image ;; (tr "workspace.undo.entry.single.image")
;; workspace.undo.entry.single.media ;; (tr "workspace.undo.entry.single.media")
;; workspace.undo.entry.single.multiple ;; (tr "workspace.undo.entry.single.multiple")
;; workspace.undo.entry.single.page ;; (tr "workspace.undo.entry.single.page")
;; workspace.undo.entry.single.path ;; (tr "workspace.undo.entry.single.path")
;; workspace.undo.entry.single.rect ;; (tr "workspace.undo.entry.single.rect")
;; workspace.undo.entry.single.shape ;; (tr "workspace.undo.entry.single.shape")
;; workspace.undo.entry.single.text ;; (tr "workspace.undo.entry.single.text")
;; workspace.undo.entry.single.typography ;; (tr "workspace.undo.entry.single.typography")
(t locale (str/format "workspace.undo.entry.%s.%s" arity attribute)))) (tr (str/format "workspace.undo.entry.%s.%s" arity attribute))))
(defn entry->message [locale entry] (defn entry->message [entry]
(let [value (entry-type->message locale (:type entry) (= :multiple (:id entry)))] (let [value (entry-type->message (:type entry) (= :multiple (:id entry)))]
(case (:operation entry) (case (:operation entry)
:new (t locale "workspace.undo.entry.new" value) :new (tr "workspace.undo.entry.new" value)
:modify (t locale "workspace.undo.entry.modify" value) :modify (tr "workspace.undo.entry.modify" value)
:delete (t locale "workspace.undo.entry.delete" value) :delete (tr "workspace.undo.entry.delete" value)
:move (t locale "workspace.undo.entry.move" value) :move (tr "workspace.undo.entry.move" value)
(t locale "workspace.undo.entry.unknown" value)))) (tr "workspace.undo.entry.unknown" value))))
(defn entry->icon [{:keys [type]}] (defn entry->icon [{:keys [type]}]
(case type (case type
@ -284,8 +284,9 @@
nil)])) nil)]))
(mf/defc history-entry [{:keys [locale entry idx-entry disabled? current?]}] (mf/defc history-entry
{::mf/props :obj} {::mf/props :obj}
[{:keys [entry idx-entry disabled? current?]}]
(let [hover? (mf/use-state false) (let [hover? (mf/use-state false)
show-detail? (mf/use-state false) show-detail? (mf/use-state false)
toggle-show-detail toggle-show-detail
@ -309,7 +310,7 @@
[:div {:class (stl/css :history-entry-summary)} [:div {:class (stl/css :history-entry-summary)}
[:div {:class (stl/css :history-entry-summary-icon)} [:div {:class (stl/css :history-entry-summary-icon)}
(entry->icon entry)] (entry->icon entry)]
[:div {:class (stl/css :history-entry-summary-text)} (entry->message locale entry)] [:div {:class (stl/css :history-entry-summary-text)} (entry->message entry)]
(when (:detail entry) (when (:detail entry)
[:div {:class (stl/css-case :history-entry-summary-button true [:div {:class (stl/css-case :history-entry-summary-button true
:button-opened @show-detail?) :button-opened @show-detail?)
@ -320,9 +321,9 @@
(when @show-detail? (when @show-detail?
[:& history-entry-details {:entry entry}])])) [:& history-entry-details {:entry entry}])]))
(mf/defc history-toolbox [] (mf/defc history-toolbox
(let [locale (mf/deref i18n/locale) []
objects (mf/deref refs/workspace-page-objects) (let [objects (mf/deref refs/workspace-page-objects)
{:keys [items index]} (mf/deref workspace-undo) {:keys [items index]} (mf/deref workspace-undo)
entries (parse-entries items objects) entries (parse-entries items objects)
toggle-history toggle-history
@ -331,18 +332,17 @@
(vary-meta assoc ::ev/origin "history-toolbox"))))] (vary-meta assoc ::ev/origin "history-toolbox"))))]
[:div {:class (stl/css :history-toolbox)} [:div {:class (stl/css :history-toolbox)}
[:div {:class (stl/css :history-toolbox-title)} [:div {:class (stl/css :history-toolbox-title)}
[:span (t locale "workspace.undo.title")] [:span (tr "workspace.undo.title")]
[:div {:class (stl/css :close-button) [:div {:class (stl/css :close-button)
:on-click toggle-history} :on-click toggle-history}
i/close]] i/close]]
(if (empty? entries) (if (empty? entries)
[:div {:class (stl/css :history-entry-empty)} [:div {:class (stl/css :history-entry-empty)}
[:div {:class (stl/css :history-entry-empty-icon)} i/history] [:div {:class (stl/css :history-entry-empty-icon)} i/history]
[:div {:class (stl/css :history-entry-empty-msg)} (t locale "workspace.undo.empty")]] [:div {:class (stl/css :history-entry-empty-msg)} (tr "workspace.undo.empty")]]
[:ul {:class (stl/css :history-entries)} [:ul {:class (stl/css :history-entries)}
(for [[idx-entry entry] (->> entries (map-indexed vector) reverse)] #_[i (range 0 10)] (for [[idx-entry entry] (->> entries (map-indexed vector) reverse)] #_[i (range 0 10)]
[:& history-entry {:key (str "entry-" idx-entry) [:& history-entry {:key (str "entry-" idx-entry)
:locale locale
:entry entry :entry entry
:idx-entry idx-entry :idx-entry idx-entry
:current? (= idx-entry index) :current? (= idx-entry index)

View file

@ -512,12 +512,12 @@
[:& dropdown {:show show :on-close on-close} [:& dropdown {:show show :on-close on-close}
[:ul {:class (stl/css-case :custom-select-dropdown true [:ul {:class (stl/css-case :custom-select-dropdown true
:not-main (not main-instance))} :not-main (not main-instance))}
(for [{:keys [msg] :as entry} menu-entries] (for [{:keys [title action]} menu-entries]
(when (some? msg) (when (some? title)
[:li {:key msg [:li {:key title
:class (stl/css :dropdown-element) :class (stl/css :dropdown-element)
:on-click (partial do-action (:action entry))} :on-click (partial do-action action)}
[:span {:class (stl/css :dropdown-label)} (tr msg)]]))]])) [:span {:class (stl/css :dropdown-label)} title]]))]]))
(mf/defc component-menu (mf/defc component-menu
{::mf/props :obj} {::mf/props :obj}

View file

@ -52,144 +52,166 @@
(defn translation-keyname (defn translation-keyname
[type keyname] [type keyname]
;; Execution time translation strings: ;; Execution time translation strings:
;; shortcut-subsection.alignment (comment
;; shortcut-subsection.edit (tr "shortcut-subsection.alignment")
;; shortcut-subsection.general-dashboard (tr "shortcut-subsection.edit")
;; shortcut-subsection.general-viewer (tr "shortcut-subsection.general-dashboard")
;; shortcut-subsection.main-menu (tr "shortcut-subsection.general-viewer")
;; shortcut-subsection.modify-layers (tr "shortcut-subsection.main-menu")
;; shortcut-subsection.navigation-dashboard (tr "shortcut-subsection.modify-layers")
;; shortcut-subsection.navigation-viewer (tr "shortcut-subsection.navigation-dashboard")
;; shortcut-subsection.navigation-workspace (tr "shortcut-subsection.navigation-viewer")
;; shortcut-subsection.panels (tr "shortcut-subsection.navigation-workspace")
;; shortcut-subsection.path-editor (tr "shortcut-subsection.panels")
;; shortcut-subsection.shape (tr "shortcut-subsection.path-editor")
;; shortcut-subsection.tools (tr "shortcut-subsection.shape")
;; shortcut-subsection.zoom-viewer (tr "shortcut-subsection.text-editor")
;; shortcut-subsection.zoom-workspace (tr "shortcut-subsection.tools")
;; shortcuts.add-comment (tr "shortcut-subsection.zoom-viewer")
;; shortcuts.add-node (tr "shortcut-subsection.zoom-workspace")
;; shortcuts.align-bottom (tr "shortcuts.add-comment")
;; shortcuts.align-hcenter (tr "shortcuts.add-node")
;; shortcuts.align-left (tr "shortcuts.align-bottom")
;; shortcuts.align-right (tr "shortcuts.align-center")
;; shortcuts.align-top (tr "shortcuts.align-hcenter")
;; shortcuts.align-vcenter (tr "shortcuts.align-justify")
;; shortcuts.artboard-selection (tr "shortcuts.align-left")
;; shortcuts.bool-difference (tr "shortcuts.align-right")
;; shortcuts.bool-exclude (tr "shortcuts.align-top")
;; shortcuts.bool-intersection (tr "shortcuts.align-vcenter")
;; shortcuts.bool-union (tr "shortcuts.artboard-selection")
;; shortcuts.bring-back (tr "shortcuts.bold")
;; shortcuts.bring-backward (tr "shortcuts.bool-difference")
;; shortcuts.bring-forward (tr "shortcuts.bool-exclude")
;; shortcuts.bring-front (tr "shortcuts.bool-intersection")
;; shortcuts.clear-undo (tr "shortcuts.bool-union")
;; shortcuts.copy (tr "shortcuts.bring-back")
;; shortcuts.create-component (tr "shortcuts.bring-backward")
;; shortcuts.create-new-project (tr "shortcuts.bring-forward")
;; shortcuts.cut (tr "shortcuts.bring-front")
;; shortcuts.decrease-zoom (tr "shortcuts.clear-undo")
;; shortcuts.delete (tr "shortcuts.copy")
;; shortcuts.delete-node (tr "shortcuts.create-component")
;; shortcuts.detach-component (tr "shortcuts.create-new-project")
;; shortcuts.draw-curve (tr "shortcuts.cut")
;; shortcuts.draw-ellipse (tr "shortcuts.decrease-zoom")
;; shortcuts.draw-frame (tr "shortcuts.delete")
;; shortcuts.draw-nodes (tr "shortcuts.delete-node")
;; shortcuts.draw-path (tr "shortcuts.detach-component")
;; shortcuts.draw-rect (tr "shortcuts.draw-curve")
;; shortcuts.draw-text (tr "shortcuts.draw-ellipse")
;; shortcuts.duplicate (tr "shortcuts.draw-frame")
;; shortcuts.escape (tr "shortcuts.draw-nodes")
;; shortcuts.export-shapes (tr "shortcuts.draw-path")
;; shortcuts.fit-all (tr "shortcuts.draw-rect")
;; shortcuts.flip-horizontal (tr "shortcuts.draw-text")
;; shortcuts.flip-vertical (tr "shortcuts.duplicate")
;; shortcuts.go-to-drafts (tr "shortcuts.escape")
;; shortcuts.go-to-libs (tr "shortcuts.export-shapes")
;; shortcuts.go-to-search (tr "shortcuts.fit-all")
;; shortcuts.group (tr "shortcuts.flip-horizontal")
;; shortcuts.h-distribute (tr "shortcuts.flip-vertical")
;; shortcuts.hide-ui (tr "shortcuts.font-size-dec")
;; shortcuts.increase-zoom (tr "shortcuts.font-size-inc")
;; shortcuts.insert-image (tr "shortcuts.go-to-drafts")
;; shortcuts.join-nodes (tr "shortcuts.go-to-libs")
;; shortcuts.make-corner (tr "shortcuts.go-to-search")
;; shortcuts.make-curve (tr "shortcuts.group")
;; shortcuts.mask (tr "shortcuts.h-distribute")
;; shortcuts.merge-nodes (tr "shortcuts.hide-ui")
;; shortcuts.move (tr "shortcuts.increase-zoom")
;; shortcuts.move-fast-down (tr "shortcuts.insert-image")
;; shortcuts.move-fast-left (tr "shortcuts.italic")
;; shortcuts.move-fast-right (tr "shortcuts.join-nodes")
;; shortcuts.move-fast-up (tr "shortcuts.letter-spacing-dec")
;; shortcuts.move-nodes (tr "shortcuts.letter-spacing-inc")
;; shortcuts.move-unit-down (tr "shortcuts.line-height-dec")
;; shortcuts.move-unit-left (tr "shortcuts.line-height-inc")
;; shortcuts.move-unit-right (tr "shortcuts.line-through")
;; shortcuts.move-unit-up (tr "shortcuts.make-corner")
;; shortcuts.next-frame (tr "shortcuts.make-curve")
;; shortcuts.opacity-0 (tr "shortcuts.mask")
;; shortcuts.opacity-1 (tr "shortcuts.merge-nodes")
;; shortcuts.opacity-2 (tr "shortcuts.move")
;; shortcuts.opacity-3 (tr "shortcuts.move-fast-down")
;; shortcuts.opacity-4 (tr "shortcuts.move-fast-left")
;; shortcuts.opacity-5 (tr "shortcuts.move-fast-right")
;; shortcuts.opacity-6 (tr "shortcuts.move-fast-up")
;; shortcuts.opacity-7 (tr "shortcuts.move-nodes")
;; shortcuts.opacity-8 (tr "shortcuts.move-unit-down")
;; shortcuts.opacity-9 (tr "shortcuts.move-unit-left")
;; shortcuts.open-color-picker (tr "shortcuts.move-unit-right")
;; shortcuts.open-comments (tr "shortcuts.move-unit-up")
;; shortcuts.open-dashboard (tr "shortcuts.next-frame")
;; shortcuts.select-prev (tr "shortcuts.opacity-0")
;; shortcuts.select-next (tr "shortcuts.opacity-1")
;; shortcuts.open-inspect (tr "shortcuts.opacity-2")
;; shortcuts.open-interactions (tr "shortcuts.opacity-3")
;; shortcuts.open-viewer (tr "shortcuts.opacity-4")
;; shortcuts.open-workspace (tr "shortcuts.opacity-5")
;; shortcuts.paste (tr "shortcuts.opacity-6")
;; shortcuts.prev-frame (tr "shortcuts.opacity-7")
;; shortcuts.redo (tr "shortcuts.opacity-8")
;; shortcuts.reset-zoom (tr "shortcuts.opacity-9")
;; shortcuts.select-all (tr "shortcuts.open-color-picker")
;; shortcuts.separate-nodes (tr "shortcuts.open-comments")
;; shortcuts.show-pixel-grid (tr "shortcuts.open-dashboard")
;; shortcuts.show-shortcuts (tr "shortcuts.open-inspect")
;; shortcuts.snap-nodes (tr "shortcuts.open-interactions")
;; shortcuts.snap-pixel-grid (tr "shortcuts.open-viewer")
;; shortcuts.start-editing (tr "shortcuts.open-workspace")
;; shortcuts.start-measure (tr "shortcuts.paste")
;; shortcuts.stop-measure (tr "shortcuts.prev-frame")
;; shortcuts.text-align-center (tr "shortcuts.redo")
;; shortcuts.text-align-left (tr "shortcuts.reset-zoom")
;; shortcuts.text-align-justify (tr "shortcuts.scale")
;; shortcuts.text-align-right (tr "shortcuts.search-placeholder")
;; shortcuts.thumbnail-set (tr "shortcuts.select-all")
;; shortcuts.toggle-alignment (tr "shortcuts.select-next")
;; shortcuts.toggle-assets (tr "shortcuts.select-parent-layer")
;; shortcuts.toggle-colorpalette (tr "shortcuts.select-prev")
;; shortcuts.toggle-focus-mode (tr "shortcuts.separate-nodes")
;; shortcuts.toggle-guides (tr "shortcuts.show-pixel-grid")
;; shortcuts.toggle-history (tr "shortcuts.show-shortcuts")
;; shortcuts.toggle-layers (tr "shortcuts.snap-nodes")
;; shortcuts.toggle-lock (tr "shortcuts.snap-pixel-grid")
;; shortcuts.toggle-lock-size (tr "shortcuts.start-editing")
;; shortcuts.toggle-rules (tr "shortcuts.start-measure")
;; shortcuts.scale (tr "shortcuts.stop-measure")
;; shortcuts.toggle-snap-guides (tr "shortcuts.text-align-center")
;; shortcuts.toggle-snap-ruler-guide (tr "shortcuts.text-align-justify")
;; shortcuts.toggle-textpalette (tr "shortcuts.text-align-left")
;; shortcuts.toggle-visibility (tr "shortcuts.text-align-right")
;; shortcuts.toggle-zoom-style (tr "shortcuts.thumbnail-set")
;; shortcuts.toggle-fullscreen (tr "shortcuts.toggle-alignment")
;; shortcuts.undo (tr "shortcuts.toggle-assets")
;; shortcuts.ungroup (tr "shortcuts.toggle-colorpalette")
;; shortcuts.unmask (tr "shortcuts.toggle-focus-mode")
;; shortcuts.v-distribute (tr "shortcuts.toggle-fullscreen")
;; shortcuts.zoom-selected (tr "shortcuts.toggle-guides")
;; shortcuts.toggle-layout-grid (tr "shortcuts.toggle-history")
(tr "shortcuts.toggle-layers")
(tr "shortcuts.toggle-layout-flex")
(tr "shortcuts.toggle-layout-grid")
(tr "shortcuts.toggle-lock")
(tr "shortcuts.toggle-lock-size")
(tr "shortcuts.toggle-rulers")
(tr "shortcuts.toggle-rules")
(tr "shortcuts.toggle-snap-guides")
(tr "shortcuts.toggle-snap-ruler-guide")
(tr "shortcuts.toggle-textpalette")
(tr "shortcuts.toggle-theme")
(tr "shortcuts.toggle-visibility")
(tr "shortcuts.toggle-zoom-style")
(tr "shortcuts.underline")
(tr "shortcuts.undo")
(tr "shortcuts.ungroup")
(tr "shortcuts.unmask")
(tr "shortcuts.v-distribute")
(tr "shortcuts.zoom-lense-decrease")
(tr "shortcuts.zoom-lense-increase")
(tr "shortcuts.zoom-selected"))
(let [translat-pre (case type (let [translat-pre (case type
:sc "shortcuts." :sc "shortcuts."
:sec "shortcut-section." :sec "shortcut-section."

View file

@ -30,8 +30,9 @@
[:div {:class (stl/css :viewport-actions)} [:div {:class (stl/css :viewport-actions)}
[:div {:class (stl/css :viewport-actions-container)} [:div {:class (stl/css :viewport-actions-container)}
[:div {:class (stl/css :viewport-actions-title)} [:div {:class (stl/css :viewport-actions-title)}
[:& i18n/tr-html {:tag-name "span" [:> i18n/tr-html*
:label "workspace.top-bar.view-only"}]] {:tag-name "span"
:content (tr "workspace.top-bar.view-only")}]]
[:button {:class (stl/css :done-btn) [:button {:class (stl/css :done-btn)
:on-click handle-close-view-mode} :on-click handle-close-view-mode}
(tr "workspace.top-bar.read-only.done")]]])) (tr "workspace.top-bar.read-only.done")]]]))

View file

@ -12,7 +12,6 @@
[app.config :as cfg] [app.config :as cfg]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.globals :as globals] [app.util.globals :as globals]
[app.util.object :as obj]
[app.util.storage :refer [storage]] [app.util.storage :refer [storage]]
[cuerdas.core :as str] [cuerdas.core :as str]
[goog.object :as gobj] [goog.object :as gobj]
@ -173,15 +172,11 @@
([code] (t @locale code)) ([code] (t @locale code))
([code & args] (apply t @locale code args))) ([code & args] (apply t @locale code args)))
(mf/defc tr-html (mf/defc tr-html*
{::mf/wrap-props false} {::mf/props :obj}
[props] [{:keys [content class tag-name]}]
(let [label (obj/get props "label") (let [tag-name (d/nilv tag-name "p")]
class (obj/get props "class") [:> tag-name {:dangerouslySetInnerHTML #js {:__html content}
tag-name (obj/get props "tag-name" "p")
params (obj/get props "params" [])
html (apply tr (d/concat-vec [label] params))]
[:> tag-name {:dangerouslySetInnerHTML #js {:__html html}
:className class}])) :className class}]))
;; DEPRECATED ;; DEPRECATED

View file

@ -7923,6 +7923,7 @@ __metadata:
eventsource-parser: "npm:^1.1.2" eventsource-parser: "npm:^1.1.2"
express: "npm:^4.19.2" express: "npm:^4.19.2"
fancy-log: "npm:^2.0.0" fancy-log: "npm:^2.0.0"
getopts: "npm:^2.3.0"
gettext-parser: "npm:^8.0.0" gettext-parser: "npm:^8.0.0"
gulp: "npm:4.0.2" gulp: "npm:4.0.2"
gulp-concat: "npm:^2.6.1" gulp-concat: "npm:^2.6.1"
@ -8236,6 +8237,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"getopts@npm:^2.3.0":
version: 2.3.0
resolution: "getopts@npm:2.3.0"
checksum: 10c0/edbcbd7020e9d87dc41e4ad9add5eb3873ae61339a62431bd92a461be2c0eaa9ec33b6fd0d67fa1b44feedffcf1cf28d6f9dbdb7d604cb1617eaba146a33cbca
languageName: node
linkType: hard
"gettext-parser@npm:^8.0.0": "gettext-parser@npm:^8.0.0":
version: 8.0.0 version: 8.0.0
resolution: "gettext-parser@npm:8.0.0" resolution: "gettext-parser@npm:8.0.0"