mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
✨ Make i18n translation files load on demand
This commit is contained in:
committed by
Belén Albeza
parent
b125c7b5a3
commit
d1379c55f6
@@ -12,6 +12,8 @@
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||
|
||||
|
||||
import Components from "@target/components";
|
||||
import translations from "@public/translation.en.js";
|
||||
Components.setDefaultTranslations(translations);
|
||||
|
||||
import '../resources/public/css/ds.css';
|
||||
|
||||
export const decorators = [
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
{{/manifest}}
|
||||
|
||||
<script type="module">
|
||||
globalThis.penpotTranslations = JSON.parse({{& translations}});
|
||||
globalThis.penpotVersion = "%version%";
|
||||
globalThis.penpotBuildDate = "%buildDate%";
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,3 @@
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.penpotTranslations = JSON.parse({{& translations}});
|
||||
</script>
|
||||
|
||||
@@ -257,7 +257,7 @@ const markedOptions = {
|
||||
|
||||
marked.use(markedOptions);
|
||||
|
||||
async function readTranslations() {
|
||||
export async function compileTranslations() {
|
||||
const langs = [
|
||||
"ar",
|
||||
"ca",
|
||||
@@ -294,9 +294,10 @@ async function readTranslations() {
|
||||
["uk", "ukr_UA"],
|
||||
"ha",
|
||||
];
|
||||
const result = {};
|
||||
|
||||
for (let lang of langs) {
|
||||
const result = {};
|
||||
|
||||
let filename = `${lang}.po`;
|
||||
if (l.isArray(lang)) {
|
||||
filename = `${lang[1]}.po`;
|
||||
@@ -315,11 +316,6 @@ async function readTranslations() {
|
||||
for (let key of Object.keys(trdata)) {
|
||||
if (key === "") continue;
|
||||
const comments = trdata[key].comments || {};
|
||||
|
||||
if (l.isNil(result[key])) {
|
||||
result[key] = {};
|
||||
}
|
||||
|
||||
const isMarkdown = l.includes(comments.flag, "markdown");
|
||||
|
||||
const msgs = trdata[key].msgstr;
|
||||
@@ -329,9 +325,9 @@ async function readTranslations() {
|
||||
message = marked.parseInline(message);
|
||||
}
|
||||
|
||||
result[key][lang] = message;
|
||||
result[key] = message;
|
||||
} else {
|
||||
result[key][lang] = msgs.map((item) => {
|
||||
result[key] = msgs.map((item) => {
|
||||
if (isMarkdown) {
|
||||
return marked.parseInline(item);
|
||||
} else {
|
||||
@@ -340,22 +336,12 @@ async function readTranslations() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
|
||||
const outputDir = "resources/public/js/";
|
||||
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
|
||||
await fs.writeFile(outputFile, esm);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function filterTranslations(translations, langs = [], keyFilter) {
|
||||
const filteredEntries = Object.entries(translations)
|
||||
.filter(([translationKey, _]) => keyFilter(translationKey))
|
||||
.map(([translationKey, value]) => {
|
||||
const langEntries = Object.entries(value).filter(([lang, _]) =>
|
||||
langs.includes(lang),
|
||||
);
|
||||
return [translationKey, Object.fromEntries(langEntries)];
|
||||
});
|
||||
|
||||
return Object.fromEntries(filteredEntries);
|
||||
}
|
||||
|
||||
async function generateSvgSprite(files, prefix) {
|
||||
@@ -407,14 +393,6 @@ async function generateTemplates() {
|
||||
const isDebug = process.env.NODE_ENV !== "production";
|
||||
await fs.mkdir("./resources/public/", { recursive: true });
|
||||
|
||||
let translations = await readTranslations();
|
||||
const storybookTranslations = JSON.stringify(
|
||||
filterTranslations(translations, ["en"], (key) =>
|
||||
key.startsWith("labels."),
|
||||
),
|
||||
);
|
||||
translations = JSON.stringify(translations);
|
||||
|
||||
const manifest = await readShadowManifest();
|
||||
let content;
|
||||
|
||||
@@ -440,7 +418,6 @@ async function generateTemplates() {
|
||||
"resources/templates/index.mustache",
|
||||
{
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
isDebug,
|
||||
},
|
||||
partials,
|
||||
@@ -468,7 +445,6 @@ async function generateTemplates() {
|
||||
"resources/templates/preview-head.mustache",
|
||||
{
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(storybookTranslations),
|
||||
},
|
||||
partials,
|
||||
);
|
||||
@@ -476,14 +452,12 @@ async function generateTemplates() {
|
||||
|
||||
content = await renderTemplate("resources/templates/render.mustache", {
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
});
|
||||
|
||||
await fs.writeFile("./resources/public/render.html", content);
|
||||
|
||||
content = await renderTemplate("resources/templates/rasterizer.mustache", {
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
});
|
||||
|
||||
await fs.writeFile("./resources/public/rasterizer.html", content);
|
||||
|
||||
@@ -4,5 +4,6 @@ await h.compileStyles();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import * as h from "./_helpers.js";
|
||||
|
||||
await fs.mkdir("resources/public/js", {recursive: true});
|
||||
|
||||
await h.compileStorybookStyles();
|
||||
await h.copyAssets();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -52,6 +52,7 @@ await fs.mkdir("./resources/public/css/", { recursive: true });
|
||||
await compileSassAll();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
await h.compileTranslations();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
@@ -81,7 +82,7 @@ h.watch("resources/templates", null, async function (path) {
|
||||
log.info("watch: translations (~)");
|
||||
h.watch("translations", null, async function (path) {
|
||||
log.info("changed:", path);
|
||||
await h.compileTemplates();
|
||||
await h.compileTranslations();
|
||||
});
|
||||
|
||||
log.info("watch: assets (~)");
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
(def default-theme "default")
|
||||
(def default-language "en")
|
||||
|
||||
(def translations (obj/get global "penpotTranslations"))
|
||||
(def themes (obj/get global "penpotThemes"))
|
||||
|
||||
(def build-date (parse-build-date global))
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
(defn ^:export init
|
||||
[]
|
||||
(mw/init!)
|
||||
(i18n/init! cf/translations)
|
||||
(i18n/init)
|
||||
(cur/init-styles)
|
||||
(thr/init!)
|
||||
(init-ui)
|
||||
@@ -114,11 +114,4 @@
|
||||
[]
|
||||
(reinit))
|
||||
|
||||
;; Reload the UI when the language changes
|
||||
(add-watch
|
||||
i18n/locale "locale"
|
||||
(fn [_ _ old-value current-value]
|
||||
(when (not= old-value current-value)
|
||||
(reinit))))
|
||||
|
||||
(set! (.-stackTraceLimit js/Error) 50)
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
(let [uagent (new ua/UAParser)]
|
||||
(merge
|
||||
{:app-version (:full cf/version)
|
||||
:locale @i18n/locale}
|
||||
:locale i18n/*current-locale*}
|
||||
(let [browser (.getBrowser uagent)]
|
||||
{:browser (obj/get browser "name")
|
||||
:browser-version (obj/get browser "version")})
|
||||
@@ -98,7 +98,9 @@
|
||||
(def context
|
||||
(atom (d/without-nils (collect-context))))
|
||||
|
||||
(add-watch i18n/locale ::events #(swap! context assoc :locale %4))
|
||||
(add-watch i18n/state "events"
|
||||
(fn [_ _ _ v]
|
||||
(swap! context assoc :locale (get v :locale))))
|
||||
|
||||
;; --- EVENT TRANSLATION
|
||||
|
||||
|
||||
@@ -53,11 +53,16 @@
|
||||
(assoc :profile-id id)
|
||||
(assoc :profile profile)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(->> (rx/from (i18n/set-locale (:lang profile)))
|
||||
(rx/ignore))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(plugins.register/init)))))
|
||||
|
||||
(def profile-fetched?
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main.ui.ds
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
|
||||
@@ -44,8 +43,6 @@
|
||||
[app.util.i18n :as i18n]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(i18n/init! cf/translations)
|
||||
|
||||
(def default
|
||||
"A export used for storybook"
|
||||
(mf/object
|
||||
@@ -80,6 +77,11 @@
|
||||
:Milestone milestone*
|
||||
:MilestoneGroup milestone-group*
|
||||
:Date date*
|
||||
|
||||
:set-default-translations
|
||||
(fn [data]
|
||||
(i18n/set-translations "en" data))
|
||||
|
||||
;; meta / misc
|
||||
:meta
|
||||
{:icons (clj->js (sort icon-list))
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as log]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.modules :as mod]
|
||||
[app.util.storage :as storage]
|
||||
[cuerdas.core :as str]
|
||||
[goog.object :as gobj]
|
||||
[okulary.core :as l]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(log/set-level! :info)
|
||||
@@ -67,6 +69,12 @@
|
||||
(-> (.-language globals/navigator)
|
||||
(parse-locale))))
|
||||
|
||||
|
||||
;; Set initial translation loading state as globaly stored variable;
|
||||
;; this facilitates hot reloading
|
||||
(when-not (exists? (unchecked-get globals/global "penpotTranslations"))
|
||||
(unchecked-set globals/global "penpotTranslations" #js {}))
|
||||
|
||||
(defn- autodetect
|
||||
[]
|
||||
(let [supported (into #{} (map :value supported-locales))]
|
||||
@@ -75,43 +83,69 @@
|
||||
(if (contains? supported locale)
|
||||
locale
|
||||
(recur (rest locales)))
|
||||
cfg/default-language))))
|
||||
cf/default-language))))
|
||||
|
||||
(defonce translations #js {})
|
||||
(defonce locale (l/atom nil))
|
||||
(defn get-current
|
||||
"Get the currently memoized locale or execute the autodetection"
|
||||
[]
|
||||
(or (get storage/global ::locale) (autodetect)))
|
||||
|
||||
(add-watch locale "common.time"
|
||||
(fn [_ _ pv cv]
|
||||
(when (not= pv cv)
|
||||
(ct/set-default-locale! cv))))
|
||||
(def ^:dynamic *current-locale*
|
||||
(get-current))
|
||||
|
||||
(defn init!
|
||||
"Initialize the i18n module with translations.
|
||||
(defonce state
|
||||
(l/atom {:render 0 :locale *current-locale*}))
|
||||
|
||||
The `data` is a javascript object for performance reasons. This code
|
||||
is executed in the critical part (application bootstrap) and used in
|
||||
many parts of the application."
|
||||
[data]
|
||||
(set! translations data)
|
||||
(reset! locale (or (get storage/global ::locale) (autodetect))))
|
||||
(defn- assign-current-locale
|
||||
[state locale]
|
||||
(-> state
|
||||
(update :render inc)
|
||||
(assoc :locale locale)))
|
||||
|
||||
(defn- get-translations
|
||||
"Get globaly stored mutable object with all loaded translations"
|
||||
[]
|
||||
(unchecked-get globals/global "penpotTranslations"))
|
||||
|
||||
(defn set-locale!
|
||||
(defn set-translations
|
||||
"A helper for synchronously set translations data for specified locale"
|
||||
[locale data]
|
||||
(let [translations (get-translations)]
|
||||
(unchecked-set translations locale data)
|
||||
nil))
|
||||
|
||||
(defn- load
|
||||
[locale]
|
||||
(let [path (str "./translation." locale ".js")]
|
||||
(->> (mod/import path)
|
||||
(p/fmap (fn [result] (unchecked-get result "default")))
|
||||
(p/fnly (fn [data cause]
|
||||
(if cause
|
||||
(js/console.error "unexpected error on fetching locale" cause)
|
||||
(do
|
||||
(set! *current-locale* locale)
|
||||
(set-translations locale data)
|
||||
(swap! state assign-current-locale locale))))))))
|
||||
|
||||
(defn init
|
||||
"Initialize the i18n module"
|
||||
[]
|
||||
(load *current-locale*))
|
||||
|
||||
(defn set-locale
|
||||
[lname]
|
||||
(if (or (nil? lname)
|
||||
(str/empty? lname))
|
||||
(let [lname (autodetect)]
|
||||
(swap! storage/global dissoc ::locale)
|
||||
(reset! locale lname))
|
||||
(let [supported (into #{} (map :value) supported-locales)
|
||||
lname (loop [locales (seq (parse-locale lname))]
|
||||
(if-let [locale (first locales)]
|
||||
(if (contains? supported locale)
|
||||
locale
|
||||
(recur (rest locales)))
|
||||
cfg/default-language))]
|
||||
(swap! storage/global assoc ::locale lname)
|
||||
(reset! locale lname))))
|
||||
(let [lname (if (or (nil? lname)
|
||||
(str/empty? lname))
|
||||
(autodetect)
|
||||
(let [supported (into #{} (map :value) supported-locales)]
|
||||
(loop [locales (seq (parse-locale lname))]
|
||||
(if-let [locale (first locales)]
|
||||
(if (contains? supported locale)
|
||||
locale
|
||||
(recur (rest locales)))
|
||||
cf/default-language))))]
|
||||
|
||||
(load lname)))
|
||||
|
||||
(deftype C [val]
|
||||
IDeref
|
||||
@@ -136,22 +170,24 @@
|
||||
|
||||
(defn t
|
||||
([locale code]
|
||||
(let [code (name code)
|
||||
value (gobj/getValueByKeys translations code locale)]
|
||||
(let [translations (get-translations)
|
||||
code (d/name code)
|
||||
value (gobj/getValueByKeys translations locale code)]
|
||||
(if (empty-string? value)
|
||||
(if (= cfg/default-language locale)
|
||||
(if (= cf/default-language locale)
|
||||
code
|
||||
(t cfg/default-language code))
|
||||
(t cf/default-language code))
|
||||
(if (array? value)
|
||||
(aget value 0)
|
||||
value))))
|
||||
([locale code & args]
|
||||
(let [code (name code)
|
||||
value (gobj/getValueByKeys translations code locale)]
|
||||
(let [translations (get-translations)
|
||||
code (d/name code)
|
||||
value (gobj/getValueByKeys translations locale code)]
|
||||
(if (empty-string? value)
|
||||
(if (= cfg/default-language locale)
|
||||
(if (= cf/default-language locale)
|
||||
code
|
||||
(apply t cfg/default-language code args))
|
||||
(apply t cf/default-language code args))
|
||||
(let [plural (first (filter c? args))
|
||||
value (if (array? value)
|
||||
(if (= @plural 1) (aget value 0) (aget value 1))
|
||||
@@ -159,8 +195,8 @@
|
||||
(apply str/fmt value (map #(if (c? %) @% %) args)))))))
|
||||
|
||||
(defn tr
|
||||
([code] (t @locale code))
|
||||
([code & args] (apply t @locale code args)))
|
||||
([code] (t *current-locale* code))
|
||||
([code & args] (apply t *current-locale* code args)))
|
||||
|
||||
(mf/defc tr-html*
|
||||
{::mf/props :obj}
|
||||
@@ -170,8 +206,9 @@
|
||||
:className class
|
||||
:on-click on-click}]))
|
||||
|
||||
;; DEPRECATED
|
||||
(defn use-locale
|
||||
[]
|
||||
(mf/deref locale))
|
||||
|
||||
(add-watch state "common.time"
|
||||
(fn [_ _ pv cv]
|
||||
(let [pv (get pv :locale)
|
||||
cv (get cv :locale)]
|
||||
(when (not= pv cv)
|
||||
(ct/set-default-locale! cv)))))
|
||||
|
||||
@@ -47,6 +47,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@target": resolve(__dirname, "./target/storybook"),
|
||||
"@public": resolve(__dirname, "./resources/public/js/"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user