0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

🎉 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",
"express": "^4.19.2",
"fancy-log": "^2.0.0",
"getopts": "^2.3.0",
"gettext-parser": "^8.0.0",
"gulp": "4.0.2",
"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.link :as lk]
[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.storage :as sto]
[beicon.v2.core :as rx]
@ -197,10 +197,11 @@
[]
(let [terms-label
(mf/html
[:& tr-html
[:> i18n/tr-html*
{:tag-name "div"
:label "auth.terms-and-privacy-agreement"
:params [cf/terms-of-service-uri cf/privacy-policy-uri]}])]
:content (tr "auth.terms-and-privacy-agreement"
cf/terms-of-service-uri
cf/privacy-policy-uri)}])]
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy

View file

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

View file

@ -167,7 +167,7 @@
[:div {:class (stl/css :dashboard-fonts-hero)}
[:div {:class (stl/css :desc)}
[: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)
:on-click on-click

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7923,6 +7923,7 @@ __metadata:
eventsource-parser: "npm:^1.1.2"
express: "npm:^4.19.2"
fancy-log: "npm:^2.0.0"
getopts: "npm:^2.3.0"
gettext-parser: "npm:^8.0.0"
gulp: "npm:4.0.2"
gulp-concat: "npm:^2.6.1"
@ -8236,6 +8237,13 @@ __metadata:
languageName: node
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":
version: 8.0.0
resolution: "gettext-parser@npm:8.0.0"