diff --git a/docs/02-Frontend-Guide.md b/docs/02-Frontend-Guide.md index 143e0e5cb..8124474a8 100644 --- a/docs/02-Frontend-Guide.md +++ b/docs/02-Frontend-Guide.md @@ -2,3 +2,117 @@ This guide intends to explain the essential details of the frontend application. + +**TODO** + + +## Translations (I18N) ## + +### How it Works ### + +All the translation strings of this application are stored in +`resources/locales.json` file. It has a self explanatory format that +looks like this: + +```json +{ + "auth.email-or-username" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:61" ], + "translations" : { + "en" : "Email or Username", + "fr" : "adresse email ou nom d'utilisateur" + } + }, + "ds.num-projects" : { + "translations": { + "en": ["1 project", "%s projects"] + } + }, +} +``` + +For development convenience, you can forget about the specific format +of that file, and just add a simple key-value entry pairs like this: + +``` +{ + [...], + "foo1": "bar1", + "foo2": "bar2" +} +``` + +The file is automatically bundled into the `index.html` file on +compile time (in development and production). The bundled content is a +simplified version of this data structure for avoid load unnecesary +data. + +The development environment has a watch process that detect changes on +that file and recompiles the `index.html`. **There are no hot reload +for translations strings**, you just need to refresh the browser tab +for refresh the translations in the running the application. + +If you have used the short (key-value) format, the watch process will +automatically convert it to the apropriate format before generate the +`index.html`. + +Finally, when you have finished to adding texts, execute the following +command for reformat the file, and track the usage locations (the +"used-in" list) before commiting the file into the repository: + +```bash +clojure -Adev translations.clj collectmessages src/uxbox/main/ resources/locales.json +``` + +NOTE: Later, we will need to think and implement the way to export and +import to other formats (mainly for transifex and similar services +compatibility). + + +### How to use it ### + +You have two aproaches for translate strings: one for general purpose +and other specific for React components (that leverages reactivity for +language changes). + +The `uxbox.util.i18n/tr` is the general purpose function. This is a +simple use case example: + +```clojure +(require '[uxbox.util.i18n :as i18n]) + +(i18n/tr "auth.email-or-username") +;; => "Email or Username" +``` + +If you have defined plurals for some translation resource, then you +need to pass an additional parameter marked as counter in order to +allow the system know when to show the plural: + +```clojure +(require '[uxbox.util.i18n :as i18n]) + +(i18n/tr "ds.num-projects" (i18n/c 10)) +;; => "10 projects" + +(i18n/tr "ds.num-projects" (i18n/c 1)) +;; => "1 project" +``` + +For React components, you have `uxbox.util.i18n/use-translations` hook: + +```clojure +(mf/defc my-component + [props] + (let [tr (i18n/use-translations)] + [:div + [:span (tr "auth.email-or-username")]])) +``` + +You can use the general purpose function in React component but when +language is changed the component will not be rerendered +automatically. + + + + diff --git a/frontend/deps.edn b/frontend/deps.edn index 2e8a92d1f..21f5ab9cf 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -15,7 +15,7 @@ funcool/cuerdas {:mvn/version "2.2.0"} funcool/lentes {:mvn/version "1.4.0-SNAPSHOT"} funcool/potok {:mvn/version "2.8.0-SNAPSHOT"} - funcool/promesa {:mvn/version "4.0.2"} + funcool/promesa {:mvn/version "5.0.0"} funcool/rumext {:mvn/version "2.0.0-SNAPSHOT"} } :paths ["src" "vendor" "resources" "../common"] @@ -25,7 +25,9 @@ {com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"} com.bhauman/rebel-readline {:mvn/version "0.1.4"} com.bhauman/figwheel-main {:mvn/version "0.2.3"} - org.clojure/tools.namespace {:mvn/version "0.3.1"}} + org.clojure/tools.namespace {:mvn/version "0.3.1"} + metosin/jsonista {:mvn/version "0.2.5"} + funcool/datoteka {:mvn/version "1.2.0"}} :extra-paths ["test"]} :repl diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 69bc0d117..80e18eb6c 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -7,6 +7,8 @@ const rename = require("gulp-rename"); const gulpif = require("gulp-if"); const gzip = require("gulp-gzip"); const cleancss = require("gulp-clean-css"); +const fs = require("fs"); +const l = require("lodash"); const paths = {}; paths.app = "./resources/"; @@ -79,6 +81,23 @@ gulp.task("scss:view", scssPipeline({ gulp.task("scss", gulp.parallel("scss:main", "scss:view")); +function readLocales() { + const path = __dirname + "/resources/locales.json"; + const content = JSON.parse(fs.readFileSync(path, {encoding: "utf8"})); + + let result = {}; + for (let key of Object.keys(content)) { + const item = content[key]; + if (l.isString(item)) { + result[key] = {"en": item}; + } else if (l.isPlainObject(item) && l.isPlainObject(item.translations)) { + result[key] = item.translations; + } + } + + return JSON.stringify(result); +} + // Templates function templatePipeline(options) { @@ -86,8 +105,12 @@ function templatePipeline(options) { const input = options.input; const output = options.output; const ts = Math.floor(new Date()); + + const locales = readLocales(); + const tmpl = mustache({ - ts: ts + ts: ts, + tr: JSON.stringify(locales), }); return gulp.src(input) @@ -98,27 +121,28 @@ function templatePipeline(options) { } gulp.task("template:main", templatePipeline({ - input: paths.app + "index.mustache", - output: paths.output, + input: paths.app + "templates/index.mustache", + output: paths.output })); gulp.task("template:view", templatePipeline({ - input: paths.app + "view.mustache", - output: paths.output + "view/", - jspath: "/js/view.js", - csspath: "/css/view.css" + input: paths.app + "templates/view.mustache", + output: paths.output + "view/" })); -gulp.task("template", gulp.parallel("template:view", "template:main")); +gulp.task("templates", gulp.parallel("template:view", "template:main")); // Entry Point gulp.task("watch:main", function() { gulp.watch(paths.scss, gulp.task("scss")); + gulp.watch([paths.app + "templates/*.mustache", + paths.app + "locales.json"], + gulp.task("templates")); }); gulp.task("watch", gulp.series( - gulp.parallel("scss", "template"), + gulp.parallel("scss", "templates"), gulp.task("watch:main") )); @@ -142,7 +166,7 @@ gulp.task("dist:template:view", templatePipeline({ output: paths.dist + "view/", })); -gulp.task("dist:template", gulp.parallel("dist:template:view", "dist:template:main")); +gulp.task("dist:templates", gulp.parallel("dist:template:view", "dist:template:main")); // Styles @@ -184,7 +208,7 @@ gulp.task("dist:gzip", function() { // Entry Point gulp.task("dist", gulp.parallel( - gulp.task("dist:template"), + gulp.task("dist:templates"), gulp.task("dist:scss"), gulp.task("dist:copy") )); diff --git a/frontend/legacy.edn b/frontend/legacy.edn new file mode 100644 index 000000000..51459f2a2 --- /dev/null +++ b/frontend/legacy.edn @@ -0,0 +1,223 @@ +{"ds.projects" "PROJECTS" + "ds.new-file" "+ New File" + "ds.project-title" "Your projects" + "ds.project-new" "+ New project" + "ds.project-thumbnail.alt" "Project title" + + "ds.ordering" "Sort by" + "ds.ordering.by-name" "name" + "ds.ordering.by-last-update" "last update" + "ds.ordering.by-creation-date" "creation date" + "ds.search.placeholder" "Search..." + "ds.uploaded-at" "Uploaded at %s" + "ds.updated-at" "Updated %s" + + "ds.confirm-title" "Are you sure?" + "ds.confirm-ok" "Ok" + "ds.confirm-cancel" "Cancel" + + "ds.multiselect-bar.copy" "Copy" + "ds.multiselect-bar.copy-to-library" "Copy to library" + "ds.multiselect-bar.move" "Move" + "ds.multiselect-bar.move-to-library" "Move to library" + "ds.multiselect-bar.rename" "Rename" + "ds.multiselect-bar.delete" "Delete" + + "ds.elements" "ELEMENTS" + "ds.icons" "ICONS" + "ds.your-icons-title" "YOUR ICONS" + "ds.store-icons-title" "ICONS STORE" + "ds.icons-collection.new" "+ New collection" + "ds.icon-new" "+ New icon" + + "ds.images" "IMAGES" + "ds.your-images-title" "YOUR IMAGES" + "ds.store-images-title" "IMAGES STORE" + "ds.images-collection.new" "+ New library" + "ds.image-new" "+ New image" + + "ds.colors" "COLORS" + "ds.your-colors-title" "YOUR COLORS" + "ds.store-colors-title" "COLORS STORE" + "ds.colors-collection.new" "+ New library" + "ds.color-new" "+ New color" + "ds.color-lightbox.title" "New color" + "ds.color-lightbox.add" "+ Add color" + + "ds.library-title" "Library: " + "ds.standard-title" "STANDARD" + "ds.your-libraries-title" "YOUR LIBRARIES" + "ds.default-library-title" "Unnamed Collection (%s)" + + "ds.project.placeholder" "New project name" + "ds.project.new" "New project" + + "common.accept" "Accept" + "common.cancel" "Cancel" + + "workspace.sidebar.icons" "Icons" + "workspace.sidebar.element-options" "Element options" + "workspace.sidebar.draw-tools" "Draw tools" + "workspace.sidebar.sitemap" "Sitemap" + "workspace.sidebar.layers" "Layers" + "workspace.sidebar.document-history" "Document history" + + "ds.page.placeholder" "Page name" + "ds.page.new" "New page" + "ds.page.edit" "Edit page" + + "ds.history.versions" "History" + "ds.history.pinned" "Pinned" + + "workspace.header.rect" "Box (Ctrl + B)" + "workspace.header.circle" "Circle (Ctrl + E)" + "workspace.header.line" "Line (Ctrl + L)" + "workspace.header.text" "Text" + "workspace.header.path" "Path" + "workspace.header.curve" "Curve" + "workspace.header.ruler" "Ruler" + "workspace.header.canvas" "Canvas" + + "ds.user.profile" "Profile" + "ds.user.password" "Password" + "ds.user.notifications" "Notifications" + "ds.user.exit" "Exit" + + "workspace.header.sitemap" "Sitemap (Ctrl + Shift + M)" + "workspace.header.draw-tools" "Draw tools (Ctrl + Shift + S)" + "workspace.header.color-palette" "Color Palette (---)" + "workspace.header.icons" "Icons (Ctrl + Shift + I)" + "workspace.header.layers" "Layers (Ctrl + Shift + L)" + "workspace.header.element-options" "Element options (Ctrl + Shift + O)" + "workspace.header.document-history" "History (Ctrl + Shift + H)" + "workspace.header.undo" "Undo (Ctrl + Z)" + "workspace.header.redo" "Redo (Ctrl + Shift + Z)" + "workspace.header.download" "Download (Ctrl + E)" + "workspace.header.image" "Image (Ctrl + I)" + "workspace.header.rules" "Rules" + "workspace.header.grid" "Grid (Ctrl + G)" + "workspace.header.grid-snap" "Snap to grid" + "workspace.header.align" "Align (Ctrl + A)" + "workspace.header.view-mode" "View mode (Ctrl + P)" + + "workspace.options.radius" "Radius" + "workspace.options.size" "Size" + "workspace.options.width" "Width" + "workspace.options.height" "Height" + "workspace.options.stroke.style" "Style" + "workspace.options.stroke.none" "None" + "workspace.options.stroke.solid" "Solid" + "workspace.options.stroke.dotted" "Dotted" + "workspace.options.stroke.dashed" "Dashed" + "workspace.options.stroke.mixed" "Mixed" + "workspace.options.position" "Position" + "workspace.options.rotation" "Rotation" + "workspace.options.opacity" "Opacity" + "workspace.options.color" "Color" + "workspace.options.background-color" "Background color" + "workspace.options.font-family" "Font Family" + "workspace.options.font-weight" "Font Size & Weight" + "workspace.options.font-size" "Font Size" + "workspace.options.line-height-letter-spacing" "Line height and Letter spacing" + "workspace.options.line-height" "Line Height" + "workspace.options.letter-spacing" "Letter Spacing" + "workspace.options.text-align" "Text Alignment" + "workspace.options.name" "Name" + "workspace.options.go" "Go go go!" + "workspace.options.measures" "Size, position & rotation" + "workspace.options.font-options" "Fonts & Font Size" + "workspace.options.rotation-radius" "Rotation & Radius" + "workspace.options.stroke" "Stroke" + "workspace.options.grid-options" "Grid settings" + + "element.fill" "Fill" + "element.text" "Text" + "element.interactions" "Interactions" + + "image.new" "New image" + "image.select" "Select from library" + "image.upload" "Upload file" + "image.import-library" "Import image from library" + + "auth.email-or-username" "Email or Username" + "auth.password" "Password" + "auth.signin" "Sign in" + "auth.forgot-password" "Forgot your password?" + "auth.no-account" "Don't have an account?" + "auth.message.recovery-token-sent" "Password recovery link sent to your inbox." + "auth.message.password-recovered" "Password successfully recovered." + + "register.fullname.placeholder" "Full Name" + "register.username.placeholder" "Username" + "register.email.placeholder" "Email" + "register.password.placeholder" "Password" + "register.get-started" "Get started" + "register.already-have-account" "Already have an account?" + + "recovery-request.username-or-email.placeholder" "username or email address" + "recovery-request.recover-password" "Recover password" + "recovery-request.go-back" "Go back!" + + "recover.password.placeholder" "Password" + "recover.recover-password" "Recover password" + "recover.go-back" "Go back!" + + "settings.profile" "PROFILE" + "settings.password" "PASSWORD" + "settings.notifications" "NOTIFICATIONS" + "settings.exit" "EXIT" + + "settings.profile.profile-saved" "Profile saved successfully!" + "settings.profile.section-basic-data" "Name, username and email" + "settings.profile.section-i18n-data" "Default language" + "settings.profile.your-name" "Your name" + "settings.profile.your-username" "Your username" + "settings.profile.your-email" "Your email" + + "settings.choose-color-theme" "Choose a theme" + "settings.profile.light-theme" "Light theme" + "settings.profile.dark-theme" "Dark theme" + "settings.profile.high-contrast-theme" "High-contrast theme" + "settings.profile.your-avatar" "Your avatar" + + "settings.password.password-saved" "Password saved successfully!" + "settings.password.wrong-old-password" "Wrong old password" + "settings.password.change-password" "Change password" + "settings.password.old-password" "Old password" + "settings.password.new-password" "New password" + "settings.password.confirm-password" "Confirm password" + + "settings.notifications.notifications-saved" "Notifications preferences saved successfully!" + "settings.notifications.prototype-notifications" "Prototype notifications" + "settings.notifications.description" "Get a roll up of prototype changes in your inbox." + "settings.notifications.none" "None" + "settings.notifications.every-hour" "Every hour" + "settings.notifications.every-day" "Every day" + + "settings.update-settings" "Update settings" + + "history.alert-message" "You are seeing version %s" + + "errors.api.form.unexpected-error" "An unexpected error occurred." + "errors.api.form.old-password-not-match" "Incorrect old password" + "errors.api.form.registration-disabled" "The registration is currently disabled." + "errors.api.form.email-already-exists" "The email is already in use by another user." + "errors.api.form.username-already-exists" "The username is already in use by another user." + "errors.api.form.user-not-exists" "Username or email does not matches any existing user." + "errors.form.required" "This field is mandatory" + "errors.form.string" "Should be string" + "errors.form.number" "Invalid number" + "errors.form.integer" "Invalid integer" + "errors.form.bool" "Should be boolean" + "errors.form.min-len" "Should be greater than %s" + "errors.form.max-len" "Should be lesser than %s" + "errors.form.color" "Should be a valid color string" + "errors.form.password-not-match" "Password does not match" + "errors.auth.unauthorized" "Username or password seems to be wrong." + "errors.auth.invalid-recovery-token" "The recovery token is invalid." + "errors.profile.update-password" "Error updating password, probably your old password is wrong." + + "errors.network" "Unable to connect to backend server." + "errors.generic" "Something wrong has happened." + "errors.conflict" "Conflict on saving data, please refresh and try again." + } diff --git a/frontend/legacy.fr.edn b/frontend/legacy.fr.edn new file mode 100644 index 000000000..e0455850f --- /dev/null +++ b/frontend/legacy.fr.edn @@ -0,0 +1,223 @@ +{"ds.projects" "PROJETS" + "ds.project-title" "Vos projets" + "ds.project-new" "+ Nouveau projet" + "ds.project-thumbnail.alt" "Titre du projet" + + "ds.ordering" "Trier par" + "ds.ordering.by-name" "nom" + "ds.ordering.by-last-update" "dernière mise à jour" + "ds.ordering.by-creation-date" "date de création" + "ds.search.placeholder" "Rechercher..." + "ds.uploaded-at" "Mise en ligne : %s" + "ds.updated-at" "Mis à jour %s" + + "ds.confirm-title" "Êtes-vous sûr ?" + "ds.confirm-ok" "Ok" + "ds.confirm-cancel" "Annuler" + + "ds.multiselect-bar.copy" "Copier" + "ds.multiselect-bar.copy-to-library" "Copier vers la librairie" + "ds.multiselect-bar.move" "Déplacer" + "ds.multiselect-bar.move-to-library" "Déplacer vers la librairie" + "ds.multiselect-bar.rename" "Renommer" + "ds.multiselect-bar.delete" "Supprimer" + + "ds.elements" "ÉLÉMENTS" + "ds.icons" "ICÔNES" + "ds.your-icons-title" "VOS ICÔNES" + "ds.store-icons-title" "BOUTIQUE" + "ds.icons-collection.new" "+ Nouvelle collection" + "ds.icon-new" "+ Nouvel icône" + + "ds.images" "IMAGES" + "ds.your-images-title" "VOS IMAGES" + "ds.store-images-title" "BOUTIQUE" + "ds.images-collection.new" "+ Nouvelle librairie" + "ds.image-new" "+ Nouvelle image" + + "ds.colors" "COULEURS" + "ds.your-colors-title" "VOS COULEURS" + "ds.store-colors-title" "BOUTIQUE" + "ds.colors-collection.new" "+ Nouvelle librairie" + "ds.color-new" "+ Nouvelle couleur" + "ds.color-lightbox.title" "Nouvelle couleur" + "ds.color-lightbox.add" "+ Ajouter couleur" + + "ds.library-title" "Librairie : " + "ds.standard-title" "STANDARD" + "ds.your-libraries-title" "VOS LIBRAIRIES" + "ds.default-library-title" "Collection sans nom (%s)" + + "ds.project.placeholder" "Nom du nouveau projet" + "ds.project.new" "Nouveau projet" + + + "ds.accept" "Accepter" + "ds.cancel" "Annuler" + + "workspace.sidebar.icons" "Icônes" + "workspace.sidebar.element-options" "Options d'élément" + "workspace.sidebar.draw-tools" "Outils de dessin" + "workspace.sidebar.sitemap" "Plan du site" + "workspace.sidebar.layers" "Couches" + "workspace.sidebar.document-history" "Historique du document" + + "ds.page.placeholder" "Nom de la page" + "ds.page.new" "Nouvelle page" + "ds.page.edit" "Éditer la page" + + "ds.history.versions" "Historique" + "ds.history.pinned" "Épinglés" + + "workspace.header.rect" "Boîte (Ctrl + B)" + "workspace.header.circle" "Cercle (Ctrl + E)" + "workspace.header.line" "Ligne (Ctrl + L)" + "workspace.header.text" "Texte" + "workspace.header.path" "Chemin" + "workspace.header.curve" "Courbe" + "workspace.header.ruler" "Règle" + "workspace.header.canvas" "Calque" + + "ds.user.profile" "Profil" + "ds.user.password" "Mot de passe" + "ds.user.notifications" "Notifications" + "ds.user.exit" "Quitter" + + "workspace.header.sitemap" "Plan du site (Ctrl + Maj + M)" + "workspace.header.draw-tools" "Outils de dessin (Ctrl + Maj + S)" + "workspace.header.color-palette" "Palette de couleurs (---)" + "workspace.header.icons" "Icônes (Ctrl + Maj + I)" + "workspace.header.layers" "Couches (Ctrl + Maj + L)" + "workspace.header.element-options" "Options d'élément (Ctrl + Maj + O)" + "workspace.header.document-history" "Historique du document (Ctrl + Maj + H)" + "workspace.header.undo" "Annuler (Ctrl + Z)" + "workspace.header.redo" "Rétablir (Ctrl + Maj + Z)" + "workspace.header.download" "Télécharger (Ctrl + E)" + "workspace.header.image" "Image (Ctrl + I)" + "workspace.header.rules" "Règles" + "workspace.header.grid" "Grille (Ctrl + G)" + "workspace.header.grid-snap" "Coller à la grille" + "workspace.header.align" "Aligner (Ctrl + A)" + "workspace.header.view-mode" "Mode visualisation (Ctrl + P)" + + "workspace.options.radius" "Rayon" + "workspace.options.size" "Taille" + "workspace.options.width" "Largeur" + "workspace.options.height" "Hauteur" + "workspace.options.stroke.style" "Style" + "workspace.options.stroke.none" "Aucun" + "workspace.options.stroke.solid" "Solide" + "workspace.options.stroke.dotted" "Pointillé" + "workspace.options.stroke.dashed" "Tiré" + "workspace.options.stroke.mixed" "Mixte" + "workspace.options.position" "Position" + "workspace.options.rotation" "Rotation" + "workspace.options.opacity" "Opacité" + "workspace.options.color" "Couleur" + "workspace.options.background-color" "Couleur d'arrière-plan" + "workspace.options.font-family" "Police de caractères" + "workspace.options.font-weight" "Taille et graisse" + "workspace.options.font-size" "Taille de police" + "workspace.options.line-height-letter-spacing" "Hauteur de ligne et Espacement de caractères" + "workspace.options.line-height" "Hauteur de ligne" + "workspace.options.letter-spacing" "Espacement de caractères" + "workspace.options.text-align" "Alignement de texte" + "workspace.options.name" "Nom" + "workspace.options.go" "C'est parti !" + "workspace.options.measures" "Taille, position et rotation" + "workspace.options.font-options" "TODO" + "workspace.options.rotation-radius" "TODO" + "workspace.options.strokestroke" "Contour" + "workspace.options.grid-options" "Paramètres de la grille" + + "element.fill" "Fond" + "element.text" "Texte" + "element.interactions" "Interactions" + + "image.new" "Nouvelle image" + "image.select" "Choisir depuis une librairie" + "image.upload" "Envoyer un fichier" + "image.import-library" "Importer une image depuis une librairie" + + "auth.email-or-username" "adresse email ou nom d'utilisateur" + "auth.password" "Mot de passe" + "auth.signin" "Se connecter" + "auth.forgot-password" "Mot de passe oublié ?" + "auth.no-account" "Vous n'avez pas de compte ?" + "auth.message.recovery-token-sent" "Lien de récupération de mot de passe envoyé." + "auth.message.password-recovered" "Mot de passe récupéré avec succès." + + "register.fullname.placeholder" "Nom complet" + "register.username.placeholder" "Nom d'utilisateur" + "register.email.placeholder" "Adresse email" + "register.password.placeholder" "Mot de passe" + "register.get-started" "Commencer" + "register.already-have-account" "Vous avez déjà un compte ?" + + "recovery-request.username-or-email.placeholder" "nom d'utilisateur ou adresse email" + "recovery-request.recover-password" "Récupérer le mot de passe" + "recovery-request.go-back" "Retour!" + + "recover.password.placeholder" "Mot de passe" + "recover.recover-password" "Récupérer le mot de passe" + "recover.go-back" "Retour!" + + "settings.profile" "PROFIL" + "settings.password" "MOT DE PASSE" + "settings.notifications" "NOTIFICATIONS" + "settings.exit" "QUITTER" + + "settings.profile.profile-saved" "Profil enregistré avec succès !" + "settings.profile.section-basic-data" "Nom, nom d'utilisateur et adresse email" + "settings.profile.section-i18n-data" "Langue par défaut" + "settings.profile.your-name" "Votre nom complet" + "settings.profile.your-username" "Votre nom d'utilisateur" + "settings.profile.your-email" "Votre adresse email" + + "settings.choose-color-theme" "Choisissez un thème" + "settings.profile.light-theme" "Thème Jour" + "settings.profile.dark-theme" "Thème Nuit" + "settings.profile.high-contrast-theme" "Thème Contraste élevé" + "settings.profile.your-avatar" "Votre avatar" + + "settings.password.password-saved" "Mot de passe enregistré avec succès !" + "settings.password.wrong-old-password" "Ancien mot de passe incorrect" + "settings.password.change-password" "Changement de mot de passe" + "settings.password.old-password" "Ancien mot de passe" + "settings.password.new-password" "Nouveau mot de passe" + "settings.password.confirm-password" "Confirmez mot de passe" + + "settings.notifications.notifications-saved" "Préférences de notifications enregistrées avec succès !" + "settings.notifications.prototype-notifications" "Notifications de prototypage" + "settings.notifications.description" "Obtenez un résumé des modifications apportées aux prototypes à votre adresse email." + "settings.notifications.none" "Aucune" + "settings.notifications.every-hour" "Chaque heure" + "settings.notifications.every-day" "Chaque jour" + + "settings.update-settings" "Mettre à jour les paramètres" + + "history.alert-message" "Vous voyez la version %s" + + "errors.api.form.unexpected-error" "Une erreur inattendue c'est produite" + "errors.api.form.old-password-not-match" "Ancien mot de passe incorrect" + "errors.api.form.registration-disabled" "L'enregistrement est actuellement désactivé." + "errors.api.form.email-already-exists" "L'email est déjà utilisé par un autre utilisateur." + "errors.api.form.username-already-exists" "Le nom d'utilisateur est déjà utilisé par un autre utilisateur." + "errors.api.form.user-not-exists" "Le nom d'utilisateur ou l'e-mail ne correspond à aucun utilisateur existant." + "errors.form.required" "Ce champ est obligatoire" + "errors.form.string" "Devrait être une chaîne de caractères" + "errors.form.number" "Nombre invalide" + "errors.form.integer" "Entier invalide" + "errors.form.bool" "Devrait être un booléen" + "errors.form.min-len" "Devrait être supérieur à %s" + "errors.form.max-len" "Devrait être inférieur à %s" + "errors.form.color" "Devrait être une couleur valide" + "errors.form.password-not-match" "Le mot de passe ne correspond pas" + "errors.auth.unauthorized" "Le nom d'utilisateur ou le mot de passe semble être faux." + "errors.auth.invalid-recovery-token" "Le jeton de récupération n'est pas valide." + "errors.profile.update-password" "Erreur lors de la mise à jour du mot de passe, votre ancien mot de passe est probablement incorrect." + + "errors.network" "Impossible de se connecter au serveur principal." + "errors.generic" "Quelque chose c'est mal passé." + "errors.conflict" "Conflit sur la sauvegarde des données, actualisez et réessayez." + } diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json new file mode 100644 index 000000000..447ec5f68 --- /dev/null +++ b/frontend/resources/locales.json @@ -0,0 +1,891 @@ +{ + "auth.email-or-username" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:61" ], + "translations" : { + "en" : "Email or Username", + "fr" : "adresse email ou nom d'utilisateur" + } + }, + "auth.forgot-password" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:83" ], + "translations" : { + "en" : "Forgot your password?", + "fr" : "Mot de passe oublié ?" + } + }, + "auth.message.password-recovered" : { + "used-in" : [ "src/uxbox/main/data/auth.cljs:178" ], + "translations" : { + "en" : "Password successfully recovered.", + "fr" : "Mot de passe récupéré avec succès." + } + }, + "auth.message.recovery-token-sent" : { + "used-in" : [ "src/uxbox/main/data/auth.cljs:141" ], + "translations" : { + "en" : "Password recovery link sent to your inbox.", + "fr" : "Lien de récupération de mot de passe envoyé." + } + }, + "auth.no-account" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:86" ], + "translations" : { + "en" : "Don't have an account?", + "fr" : "Vous n'avez pas de compte ?" + } + }, + "auth.password" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:70" ], + "translations" : { + "en" : "Password", + "fr" : "Mot de passe" + } + }, + "auth.signin" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:77" ], + "translations" : { + "en" : "Sign in", + "fr" : "Se connecter" + } + }, + "ds.accept" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/history.cljs:113" ], + "translations" : { + "en" : null, + "fr" : "Accepter" + } + }, + "ds.cancel" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/history.cljs:114" ], + "translations" : { + "en" : null, + "fr" : "Annuler" + } + }, + "ds.color-lightbox.add" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/colors.cljs:52" ], + "translations" : { + "en" : "+ Add color", + "fr" : "+ Ajouter couleur" + } + }, + "ds.color-new" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/colors.cljs:149" ], + "translations" : { + "en" : "+ New color", + "fr" : "+ Nouvelle couleur" + } + }, + "ds.colors" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:48" ], + "translations" : { + "en" : "COLORS", + "fr" : "COULEURS" + } + }, + "ds.colors-collection.new" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/colors.cljs:134" ], + "translations" : { + "en" : "+ New library", + "fr" : "+ Nouvelle librairie" + } + }, + "ds.confirm-cancel" : { + "used-in" : [ "src/uxbox/main/ui/confirm.cljs:38" ], + "translations" : { + "en" : "Cancel", + "fr" : "Annuler" + } + }, + "ds.confirm-ok" : { + "used-in" : [ "src/uxbox/main/ui/confirm.cljs:34" ], + "translations" : { + "en" : "Ok", + "fr" : "Ok" + } + }, + "ds.confirm-title" : { + "used-in" : [ "src/uxbox/main/ui/confirm.cljs:28" ], + "translations" : { + "en" : "Are you sure?", + "fr" : "Êtes-vous sûr ?" + } + }, + "ds.default-library-title" : { + "used-in" : [ "src/uxbox/main/data/images.cljs:99", "src/uxbox/main/data/icons.cljs:87", "src/uxbox/main/data/colors.cljs:68" ], + "translations" : { + "en" : "Unnamed Collection (%s)", + "fr" : "Collection sans nom (%s)" + } + }, + "ds.foobar" : { + "translations" : { + "en" : null, + "fr" : null + }, + "permanent" : true + }, + "ds.height" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs:46" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "ds.history.pinned" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/history.cljs:95" ], + "translations" : { + "en" : "Pinned", + "fr" : "Épinglés" + } + }, + "ds.history.versions" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/history.cljs:92" ], + "translations" : { + "en" : "History", + "fr" : "Historique" + } + }, + "ds.icon-new" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/icons.cljs:158" ], + "translations" : { + "en" : "+ New icon", + "fr" : "+ Nouvel icône" + } + }, + "ds.icons" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:42" ], + "translations" : { + "en" : "ICONS", + "fr" : "ICÔNES" + } + }, + "ds.icons-collection.new" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/icons.cljs:137" ], + "translations" : { + "en" : "+ New collection", + "fr" : "+ Nouvelle collection" + } + }, + "ds.image-new" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:252" ], + "translations" : { + "en" : "+ New image", + "fr" : "+ Nouvelle image" + } + }, + "ds.images" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:45" ], + "translations" : { + "en" : "IMAGES", + "fr" : "IMAGES" + } + }, + "ds.images-collection.new" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:108" ], + "translations" : { + "en" : "+ New library", + "fr" : "+ Nouvelle librairie" + } + }, + "ds.multiselect-bar.copy" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:196", "src/uxbox/main/ui/dashboard/images.cljs:171", "src/uxbox/main/ui/dashboard/colors.cljs:224", "src/uxbox/main/ui/dashboard/colors.cljs:201" ], + "translations" : { + "en" : "Copy", + "fr" : "Copier" + } + }, + "ds.multiselect-bar.copy-to-library" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:200", "src/uxbox/main/ui/dashboard/images.cljs:175", "src/uxbox/main/ui/dashboard/colors.cljs:228", "src/uxbox/main/ui/dashboard/colors.cljs:205" ], + "translations" : { + "en" : "Copy to library", + "fr" : "Copier vers la librairie" + } + }, + "ds.multiselect-bar.delete" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:190", "src/uxbox/main/ui/dashboard/colors.cljs:217" ], + "translations" : { + "en" : "Delete", + "fr" : "Supprimer" + } + }, + "ds.multiselect-bar.move" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:179", "src/uxbox/main/ui/dashboard/colors.cljs:209" ], + "translations" : { + "en" : "Move", + "fr" : "Déplacer" + } + }, + "ds.multiselect-bar.move-to-library" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:183", "src/uxbox/main/ui/dashboard/colors.cljs:213" ], + "translations" : { + "en" : "Move to library", + "fr" : "Déplacer vers la librairie" + } + }, + "ds.multiselect-bar.rename" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:187" ], + "translations" : { + "en" : "Rename", + "fr" : "Renommer" + } + }, + "ds.new-file" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/projects.cljs:143" ], + "translations" : { + "en" : "+ New File", + "fr" : null + } + }, + "ds.position" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs:52" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "ds.projects" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:39" ], + "translations" : { + "en" : "PROJECTS", + "fr" : "PROJETS" + } + }, + "ds.rotation" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs:67" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "ds.search.placeholder" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/projects.cljs:192" ], + "translations" : { + "en" : "Search...", + "fr" : "Rechercher..." + } + }, + "ds.settings.document-history" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/history.cljs:86" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "ds.settings.layers" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/layers.cljs:260" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "ds.size" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs:33" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "ds.store-colors-title" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/colors.cljs:129" ], + "translations" : { + "en" : "COLORS STORE", + "fr" : "BOUTIQUE" + } + }, + "ds.store-icons-title" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/icons.cljs:131" ], + "translations" : { + "en" : "ICONS STORE", + "fr" : "BOUTIQUE" + } + }, + "ds.store-images-title" : { + "used-in" : [ "src/uxbox/main/ui/workspace/images.cljs:146", "src/uxbox/main/ui/dashboard/images.cljs:102" ], + "translations" : { + "en" : "IMAGES STORE", + "fr" : "BOUTIQUE" + } + }, + "ds.updated-at" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/projects.cljs:110" ], + "translations" : { + "en" : "Updated %s", + "fr" : "Mis à jour %s" + } + }, + "ds.uploaded-at" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:237", "src/uxbox/main/ui/dashboard/icons.cljs:303" ], + "translations" : { + "en" : "Uploaded at %s", + "fr" : "Mise en ligne : %s" + } + }, + "ds.user.exit" : { + "used-in" : [ "src/uxbox/main/ui/users.cljs:43" ], + "translations" : { + "en" : "Exit", + "fr" : "Quitter" + } + }, + "ds.user.notifications" : { + "used-in" : [ "src/uxbox/main/ui/users.cljs:40" ], + "translations" : { + "en" : "Notifications", + "fr" : "Notifications" + } + }, + "ds.user.password" : { + "used-in" : [ "src/uxbox/main/ui/users.cljs:37" ], + "translations" : { + "en" : "Password", + "fr" : "Mot de passe" + } + }, + "ds.user.profile" : { + "used-in" : [ "src/uxbox/main/ui/users.cljs:34" ], + "translations" : { + "en" : "Profile", + "fr" : "Profil" + } + }, + "ds.width" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs:36" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "ds.your-colors-title" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/colors.cljs:126" ], + "translations" : { + "en" : "YOUR COLORS", + "fr" : "VOS COULEURS" + } + }, + "ds.your-icons-title" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/icons.cljs:128" ], + "translations" : { + "en" : "YOUR ICONS", + "fr" : "VOS ICÔNES" + } + }, + "ds.your-images-title" : { + "used-in" : [ "src/uxbox/main/ui/workspace/images.cljs:143", "src/uxbox/main/ui/dashboard/images.cljs:99" ], + "translations" : { + "en" : "YOUR IMAGES", + "fr" : "VOS IMAGES" + } + }, + "element.fill" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:44" ], + "translations" : { + "en" : "Fill", + "fr" : "Fond" + } + }, + "errors.api.form.registration-disabled" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:39" ], + "translations" : { + "en" : "The registration is currently disabled.", + "fr" : "L'enregistrement est actuellement désactivé." + } + }, + "errors.api.form.unexpected-error" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:51" ], + "translations" : { + "en" : "An unexpected error occurred.", + "fr" : "Une erreur inattendue c'est produite" + } + }, + "errors.auth.invalid-recovery-token" : { + "used-in" : [ "src/uxbox/main/data/auth.cljs:174", "src/uxbox/main/data/auth.cljs:151" ], + "translations" : { + "en" : "The recovery token is invalid.", + "fr" : "Le jeton de récupération n'est pas valide." + } + }, + "errors.auth.unauthorized" : { + "used-in" : [ "src/uxbox/main/data/auth.cljs:59" ], + "translations" : { + "en" : "Username or password seems to be wrong.", + "fr" : "Le nom d'utilisateur ou le mot de passe semble être faux." + } + }, + "errors.generic" : { + "used-in" : [ "src/uxbox/main/ui.cljs:86" ], + "translations" : { + "en" : "Something wrong has happened.", + "fr" : "Quelque chose c'est mal passé." + } + }, + "errors.network" : { + "used-in" : [ "src/uxbox/main/ui.cljs:80" ], + "translations" : { + "en" : "Unable to connect to backend server.", + "fr" : "Impossible de se connecter au serveur principal." + } + }, + "header.sitemap" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:83" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "history.alert-message" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/history.cljs:111" ], + "translations" : { + "en" : "You are seeing version %s", + "fr" : "Vous voyez la version %s" + } + }, + "image.import-library" : { + "used-in" : [ "src/uxbox/main/ui/workspace/images.cljs:137" ], + "translations" : { + "en" : "Import image from library", + "fr" : "Importer une image depuis une librairie" + } + }, + "image.new" : { + "used-in" : [ "src/uxbox/main/ui/workspace/images.cljs:71" ], + "translations" : { + "en" : "New image", + "fr" : "Nouvelle image" + } + }, + "image.select" : { + "used-in" : [ "src/uxbox/main/ui/workspace/images.cljs:75" ], + "translations" : { + "en" : "Select from library", + "fr" : "Choisir depuis une librairie" + } + }, + "image.upload" : { + "used-in" : [ "src/uxbox/main/ui/workspace/images.cljs:80" ], + "translations" : { + "en" : "Upload file", + "fr" : "Envoyer un fichier" + } + }, + "register.already-have-account" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:131" ], + "translations" : { + "en" : "Already have an account?", + "fr" : "Vous avez déjà un compte ?" + } + }, + "register.fullname.placeholder" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:72" ], + "translations" : { + "en" : "Full Name", + "fr" : "Nom complet" + } + }, + "register.get-started" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:127" ], + "translations" : { + "en" : "Get started", + "fr" : "Commencer" + } + }, + "register.password.placeholder" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:115" ], + "translations" : { + "en" : "Password", + "fr" : "Mot de passe" + } + }, + "settings.notifications" : { + "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:43" ], + "translations" : { + "en" : "NOTIFICATIONS", + "fr" : "NOTIFICATIONS" + } + }, + "settings.notifications.description" : { + "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:19" ], + "translations" : { + "en" : "Get a roll up of prototype changes in your inbox.", + "fr" : "Obtenez un résumé des modifications apportées aux prototypes à votre adresse email." + } + }, + "settings.notifications.every-day" : { + "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:38" ], + "translations" : { + "en" : "Every day", + "fr" : "Chaque jour" + } + }, + "settings.notifications.every-hour" : { + "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:32" ], + "translations" : { + "en" : "Every hour", + "fr" : "Chaque heure" + } + }, + "settings.notifications.none" : { + "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:26" ], + "translations" : { + "en" : "None", + "fr" : "Aucune" + } + }, + "settings.notifications.notifications-saved" : { + "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:18" ], + "translations" : { + "en" : "Notifications preferences saved successfully!", + "fr" : "Préférences de notifications enregistrées avec succès !" + } + }, + "settings.password" : { + "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:40" ], + "translations" : { + "en" : "PASSWORD", + "fr" : "MOT DE PASSE" + } + }, + "settings.password.change-password" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:50" ], + "translations" : { + "en" : "Change password", + "fr" : "Changement de mot de passe" + } + }, + "settings.password.confirm-password" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:79" ], + "translations" : { + "en" : "Confirm password", + "fr" : "Confirmez mot de passe" + } + }, + "settings.password.new-password" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:69" ], + "translations" : { + "en" : "New password", + "fr" : "Nouveau mot de passe" + } + }, + "settings.password.old-password" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:58" ], + "translations" : { + "en" : "Old password", + "fr" : "Ancien mot de passe" + } + }, + "settings.password.password-saved" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:33" ], + "translations" : { + "en" : "Password saved successfully!", + "fr" : "Mot de passe enregistré avec succès !" + } + }, + "settings.profile" : { + "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:37" ], + "translations" : { + "en" : "PROFILE", + "fr" : "PROFIL" + } + }, + "settings.profile.profile-saved" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:68" ], + "translations" : { + "en" : "Profile saved successfully!", + "fr" : "Profil enregistré avec succès !" + } + }, + "settings.profile.section-basic-data" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:78" ], + "translations" : { + "en" : "Name, username and email", + "fr" : "Nom, nom d'utilisateur et adresse email" + } + }, + "settings.profile.section-i18n-data" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:117" ], + "translations" : { + "en" : "Default language", + "fr" : "Langue par défaut" + } + }, + "settings.profile.your-avatar" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:159" ], + "translations" : { + "en" : "Your avatar", + "fr" : "Votre avatar" + } + }, + "settings.profile.your-email" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:111", "src/uxbox/main/ui/auth/register.cljs:101" ], + "translations" : { + "en" : "Your email", + "fr" : "Votre adresse email" + } + }, + "settings.profile.your-name" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:86" ], + "translations" : { + "en" : "Your name", + "fr" : "Votre nom complet" + } + }, + "settings.profile.your-username" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:98", "src/uxbox/main/ui/auth/register.cljs:87" ], + "translations" : { + "en" : "Your username", + "fr" : "Votre nom d'utilisateur" + } + }, + "settings.update-settings" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:130", "src/uxbox/main/ui/settings/password.cljs:86", "src/uxbox/main/ui/settings/notifications.cljs:42" ], + "translations" : { + "en" : "Update settings", + "fr" : "Mettre à jour les paramètres" + } + }, + "workspace.header.canvas" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:93" ], + "translations" : { + "en" : "Canvas", + "fr" : "Calque" + } + }, + "workspace.header.circle" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:103" ], + "translations" : { + "en" : "Circle (Ctrl + E)", + "fr" : "Cercle (Ctrl + E)" + } + }, + "workspace.header.color-palette" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:123" ], + "translations" : { + "en" : "Color Palette (---)", + "fr" : "Palette de couleurs (---)" + } + }, + "workspace.header.curve" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:118" ], + "translations" : { + "en" : "Curve", + "fr" : "Courbe" + } + }, + "workspace.header.document-history" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:143" ], + "translations" : { + "en" : "History (Ctrl + Shift + H)", + "fr" : "Historique du document (Ctrl + Maj + H)" + } + }, + "workspace.header.download" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:156" ], + "translations" : { + "en" : "Download (Ctrl + E)", + "fr" : "Télécharger (Ctrl + E)" + } + }, + "workspace.header.grid" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:170" ], + "translations" : { + "en" : "Grid (Ctrl + G)", + "fr" : "Grille (Ctrl + G)" + } + }, + "workspace.header.grid-snap" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:175" ], + "translations" : { + "en" : "Snap to grid", + "fr" : "Coller à la grille" + } + }, + "workspace.header.icons" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:128" ], + "translations" : { + "en" : "Icons (Ctrl + Shift + I)", + "fr" : "Icônes (Ctrl + Maj + I)" + } + }, + "workspace.header.image" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:161" ], + "translations" : { + "en" : "Image (Ctrl + I)", + "fr" : "Image (Ctrl + I)" + } + }, + "workspace.header.path" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:113" ], + "translations" : { + "en" : "Path", + "fr" : "Chemin" + } + }, + "workspace.header.rect" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:98" ], + "translations" : { + "en" : "Box (Ctrl + B)", + "fr" : "Boîte (Ctrl + B)" + } + }, + "workspace.header.rules" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:165" ], + "translations" : { + "en" : "Rules", + "fr" : "Règles" + } + }, + "workspace.header.text" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:108" ], + "translations" : { + "en" : "Text", + "fr" : "Texte" + } + }, + "workspace.header.view-mode" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:186" ], + "translations" : { + "en" : "View mode (Ctrl + P)", + "fr" : "Mode visualisation (Ctrl + P)" + } + }, + "workspace.options.color" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:81", "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:125", "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:47" ], + "translations" : { + "en" : "Color", + "fr" : "Couleur" + } + }, + "workspace.options.font-family" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:204" ], + "translations" : { + "en" : "Font Family", + "fr" : "Police de caractères" + } + }, + "workspace.options.font-options" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:202" ], + "translations" : { + "en" : "Fonts & Font Size", + "fr" : "TODO" + } + }, + "workspace.options.font-weight" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:213" ], + "translations" : { + "en" : "Font Size & Weight", + "fr" : "Taille et graisse" + } + }, + "workspace.options.grid-options" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:113" ], + "translations" : { + "en" : "Grid settings", + "fr" : "Paramètres de la grille" + } + }, + "workspace.options.line-height-letter-spacing" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:245" ], + "translations" : { + "en" : "Line height and Letter spacing", + "fr" : "Hauteur de ligne et Espacement de caractères" + } + }, + "workspace.options.measures" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:70", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:64", "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:65", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:62", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:115" ], + "translations" : { + "en" : "Size, position & rotation", + "fr" : "Taille, position et rotation" + } + }, + "workspace.options.opacity" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:89", "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:58" ], + "translations" : { + "en" : "Opacity", + "fr" : "Opacité" + } + }, + "workspace.options.position" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:99", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:93", "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:93", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:91", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:74" ], + "translations" : { + "en" : "Position", + "fr" : "Position" + } + }, + "workspace.options.rotation-radius" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:116", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:110", "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:108", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:108", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:92" ], + "translations" : { + "en" : "Rotation & Radius", + "fr" : "TODO" + } + }, + "workspace.options.size" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:72", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:66", "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:115", "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:69", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:64", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:38" ], + "translations" : { + "en" : "Size", + "fr" : "Taille" + } + }, + "workspace.options.stroke" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:58" ], + "translations" : { + "en" : "Stroke", + "fr" : null + } + }, + "workspace.options.stroke.dashed" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:69" ], + "translations" : { + "en" : "Dashed", + "fr" : "Tiré" + } + }, + "workspace.options.stroke.dotted" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:68" ], + "translations" : { + "en" : "Dotted", + "fr" : "Pointillé" + } + }, + "workspace.options.stroke.mixed" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:70" ], + "translations" : { + "en" : "Mixed", + "fr" : "Mixte" + } + }, + "workspace.options.stroke.none" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:66" ], + "translations" : { + "en" : "None", + "fr" : "Aucun" + } + }, + "workspace.options.stroke.solid" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:67" ], + "translations" : { + "en" : "Solid", + "fr" : "Solide" + } + }, + "workspace.options.stroke.style" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:62" ], + "translations" : { + "en" : "Style", + "fr" : "Style" + } + }, + "workspace.options.text-align" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:264" ], + "translations" : { + "en" : "Text Alignment", + "fr" : "Alignement de texte" + } + }, + "workspace.sidebar.sitemap" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/sitemap.cljs:134" ], + "translations" : { + "en" : "Sitemap", + "fr" : "Plan du site" + } + } +} diff --git a/frontend/resources/index.mustache b/frontend/resources/templates/index.mustache similarity index 92% rename from frontend/resources/index.mustache rename to frontend/resources/templates/index.mustache index 3a83f8351..df84b69cf 100644 --- a/frontend/resources/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -15,6 +15,6 @@ - + diff --git a/frontend/resources/view.mustache b/frontend/resources/templates/view.mustache similarity index 100% rename from frontend/resources/view.mustache rename to frontend/resources/templates/view.mustache diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index 2aa685b92..c767a41f1 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -10,8 +10,6 @@ [rumext.alpha :as mf] [uxbox.main.data.auth :refer [logout]] [uxbox.main.data.users :as udu] - [uxbox.main.locales.en :as en] - [uxbox.main.locales.fr :as fr] [uxbox.main.store :as st] [uxbox.main.ui :as ui] [uxbox.main.ui.lightbox :refer [lightbox]] @@ -19,7 +17,7 @@ [uxbox.main.ui.loader :refer [loader]] [uxbox.util.dom :as dom] [uxbox.util.html.history :as html-history] - [uxbox.util.i18n :as i18n :refer [tr]] + [uxbox.util.i18n :as i18n] [uxbox.util.messages :as uum] [uxbox.util.router :as rt] [uxbox.util.storage :refer [storage]] @@ -30,15 +28,15 @@ (declare reinit) (s/check-asserts true) -(i18n/update-locales! (fn [locales] - (-> locales - (assoc "en" en/locales) - (assoc "fr" fr/locales)))) +;; (i18n/update-locales! (fn [locales] +;; (-> locales +;; (assoc "en" en/locales) +;; (assoc "fr" fr/locales)))) -(i18n/on-locale-change! - (fn [new old] - (println "Locale changed from" old " to " new) - (reinit))) +;; (i18n/on-locale-change! +;; (fn [new old] +;; (println "Locale changed from" old " to " new) +;; (reinit))) ;; --- Error Handling @@ -81,7 +79,8 @@ (def app-sym (.for js/Symbol "uxbox.app")) (defn ^:export init - [] + [translations] + (i18n/init! (js/JSON.parse translations)) (unchecked-set js/window app-sym "main") (st/init) (init-ui)) diff --git a/frontend/src/uxbox/main/ui/dashboard/colors.cljs b/frontend/src/uxbox/main/ui/dashboard/colors.cljs index 6059542d9..dbea27b19 100644 --- a/frontend/src/uxbox/main/ui/dashboard/colors.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/colors.cljs @@ -40,7 +40,7 @@ [{:keys [on-submit value] :as props}] (let [local (mf/use-var value)] [:div.lightbox-body - [:h3 (tr "ds.color-lightbox.title")] + [:h3 (tr "ds.color-lightbox.title" )] [:form [:div.row-flex.center [:& colorpicker {:value (or @local "#00ccff") diff --git a/frontend/src/uxbox/main/ui/dashboard/icons.cljs b/frontend/src/uxbox/main/ui/dashboard/icons.cljs index 1eb72fc35..bbfaf3941 100644 --- a/frontend/src/uxbox/main/ui/dashboard/icons.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/icons.cljs @@ -190,75 +190,75 @@ [:li {:key (pr-str id)} [:a {:on-click #(on-select % id)} name]])]))) -(mf/def grid-options - :mixins [(mf/local) mf/memo] +;; (mf/def grid-options +;; :mixins [(mf/local) mf/memo] - :render - (fn [{:keys [::mf/local] :as own} - {:keys [id type selected] :as props}] - (letfn [(delete [] - (st/emit! (di/delete-selected))) - (on-delete [event] - (modal/show! confirm-dialog {:on-accept delete})) - (on-toggle-copy [event] - (swap! local update :show-copy-tooltip not)) - (on-toggle-move [event] - (swap! local update :show-move-tooltip not)) - (on-copy [selected] - (swap! local assoc - :show-move-tooltip false - :show-copy-tooltip false) - (st/emit! (di/copy-selected selected))) - (on-move [selected] - (swap! local assoc - :show-move-tooltip false - :show-copy-tooltip false) - (st/emit! (di/move-selected selected))) - (on-rename [event] - (let [selected (first selected)] - (st/emit! (di/update-opts :edition selected))))] +;; :render +;; (fn [{:keys [::mf/local] :as own} +;; {:keys [id type selected] :as props}] +;; (letfn [(delete [] +;; (st/emit! (di/delete-selected))) +;; (on-delete [event] +;; (modal/show! confirm-dialog {:on-accept delete})) +;; (on-toggle-copy [event] +;; (swap! local update :show-copy-tooltip not)) +;; (on-toggle-move [event] +;; (swap! local update :show-move-tooltip not)) +;; (on-copy [selected] +;; (swap! local assoc +;; :show-move-tooltip false +;; :show-copy-tooltip false) +;; (st/emit! (di/copy-selected selected))) +;; (on-move [selected] +;; (swap! local assoc +;; :show-move-tooltip false +;; :show-copy-tooltip false) +;; (st/emit! (di/move-selected selected))) +;; (on-rename [event] +;; (let [selected (first selected)] +;; (st/emit! (di/update-opts :edition selected))))] - ;; MULTISELECT OPTIONS BAR - [:div.multiselect-bar - (if (or (= type :own) (nil? id)) - ;; if editable - [:div.multiselect-nav {} - [:span.move-item.tooltip.tooltip-top - {:alt (tr "ds.multiselect-bar.copy") - :on-click on-toggle-copy} - (when (:show-copy-tooltip @local) - (grid-options-tooltip {:selected id - :title (tr "ds.multiselect-bar.copy-to-library") - :on-select on-copy})) - i/copy] - [:span.move-item.tooltip.tooltip-top - {:alt (tr "ds.multiselect-bar.move") - :on-click on-toggle-move} - (when (:show-move-tooltip @local) - (grid-options-tooltip {:selected id - :title (tr "ds.multiselect-bar.move-to-library") - :on-select on-move})) - i/move] - (when (= 1 (count selected)) - [:span.move-item.tooltip.tooltip-top - {:alt (tr "ds.multiselect-bar.rename") - :on-click on-rename} - i/pencil]) - [:span.delete.tooltip.tooltip-top - {:alt (tr "ds.multiselect-bar.delete") - :on-click on-delete} - i/trash]] +;; ;; MULTISELECT OPTIONS BAR +;; [:div.multiselect-bar +;; (if (or (= type :own) (nil? id)) +;; ;; if editable +;; [:div.multiselect-nav {} +;; [:span.move-item.tooltip.tooltip-top +;; {:alt (tr "ds.multiselect-bar.copy") +;; :on-click on-toggle-copy} +;; (when (:show-copy-tooltip @local) +;; (grid-options-tooltip {:selected id +;; :title (tr "ds.multiselect-bar.copy-to-library") +;; :on-select on-copy})) +;; i/copy] +;; [:span.move-item.tooltip.tooltip-top +;; {:alt (tr "ds.multiselect-bar.move") +;; :on-click on-toggle-move} +;; (when (:show-move-tooltip @local) +;; (grid-options-tooltip {:selected id +;; :title (tr "ds.multiselect-bar.move-to-library") +;; :on-select on-move})) +;; i/move] +;; (when (= 1 (count selected)) +;; [:span.move-item.tooltip.tooltip-top +;; {:alt (tr "ds.multiselect-bar.rename") +;; :on-click on-rename} +;; i/pencil]) +;; [:span.delete.tooltip.tooltip-top +;; {:alt (tr "ds.multiselect-bar.delete") +;; :on-click on-delete} +;; i/trash]] - ;; if not editable - [:div.multiselect-nav - [:span.move-item.tooltip.tooltip-top - {:alt (tr "ds.multiselect-bar.copy") - :on-click on-toggle-copy} - (when (:show-copy-tooltip @local) - (grid-options-tooltip {:selected id - :title (tr "ds.multiselect-bar.copy-to-library") - :on-select on-copy})) - i/organize]])]))) +;; ;; if not editable +;; [:div.multiselect-nav +;; [:span.move-item.tooltip.tooltip-top +;; {:alt (tr "ds.multiselect-bar.copy") +;; :on-click on-toggle-copy} +;; (when (:show-copy-tooltip @local) +;; (grid-options-tooltip {:selected id +;; :title (tr "ds.multiselect-bar.copy-to-library") +;; :on-select on-copy})) +;; i/organize]])]))) ;; --- Grid Item @@ -349,7 +349,7 @@ :coll coll :opts opts}] (when (seq (:selected opts)) - [:& grid-options {:id id :type type :selected (:selected opts)}])]])) + #_[:& grid-options {:id id :type type :selected (:selected opts)}])]])) ;; --- Icons Page diff --git a/frontend/src/uxbox/main/ui/dashboard/projects.cljs b/frontend/src/uxbox/main/ui/dashboard/projects.cljs index 81bc5fc5b..5f7b63fde 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects.cljs @@ -19,7 +19,6 @@ [uxbox.main.ui.modal :as modal] [uxbox.main.ui.keyboard :as kbd] [uxbox.main.ui.confirm :refer [confirm-dialog]] - [uxbox.main.ui.dashboard.projects-forms :refer [create-project-dialog]] [uxbox.main.ui.dashboard.common :as common] [uxbox.util.data :refer [read-string]] [uxbox.util.dom :as dom] diff --git a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs deleted file mode 100644 index dda2af45d..000000000 --- a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs +++ /dev/null @@ -1,72 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2015-2019 Andrey Antukh -;; Copyright (c) 2015-2019 Juan de la Cruz - -(ns uxbox.main.ui.dashboard.projects-forms - (:require - [cljs.spec.alpha :as s] - [rumext.alpha :as mf] - [uxbox.builtins.icons :as i] - [uxbox.main.data.projects :as udp] - [uxbox.main.store :as st] - [uxbox.main.ui.modal :as modal] - [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] - [uxbox.util.i18n :as t :refer [tr]])) - -(s/def ::name ::fm/not-empty-string) -(s/def ::width ::fm/number-str) -(s/def ::height ::fm/number-str) - -(s/def ::project-form - (s/keys :req-un [::name])) - -(def defaults {:name ""}) - -;; --- Create Project Form - -(defn- on-submit - [event form] - (dom/prevent-default event) - (let [data (:clean-data form)] - (st/emit! (udp/create-project data)) - (modal/hide!))) - -(defn- swap-size - [event {:keys [data] :as form}] - (swap! data assoc - :width (:height data) - :height (:width data))) - -(mf/defc create-project-form - [props] - (let [{:keys [data] :as form} (fm/use-form ::project-form defaults)] - [:form {:on-submit #(on-submit % form)} - [:input.input-text - {:placeholder (tr "ds.project.placeholder") - :type "text" - :name "name" - :value (:name data) - :class (fm/error-class form :name) - :on-blur (fm/on-input-blur form :name) - :on-change (fm/on-input-change form :name) - :auto-focus true}] - ;; Submit - [:input#project-btn.btn-primary - {:value (tr "ds.go") - :class (when-not (:valid form) "btn-disabled") - :disabled (not (:valid form)) - :type "submit"}]])) - -;; --- Create Project Lightbox - -(mf/defc create-project-dialog - [props] - [:div.lightbox-body - [:h3 (tr "ds.project.new")] - [:& create-project-form] - [:a.close {:on-click modal/hide!} i/close]]) - diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index e532dd8bb..340b4e32c 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -21,7 +21,7 @@ [uxbox.main.ui.workspace.sortable :refer [use-sortable]] [uxbox.util.data :refer [classnames enumerate]] [uxbox.util.dom :as dom] - [uxbox.util.i18n :refer [tr]] + [uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.router :as rt])) ;; --- Page Item @@ -128,7 +128,8 @@ (mf/defc sitemap-toolbox [{:keys [file page] :as props}] - (let [on-create-click #(st/emit! dp/create-empty-page)] + (let [on-create-click #(st/emit! dp/create-empty-page) + tr (i18n/use-translations)] [:div.sitemap.tool-window [:div.tool-window-bar [:span (tr "workspace.sidebar.sitemap")] diff --git a/frontend/src/uxbox/util/i18n.cljs b/frontend/src/uxbox/util/i18n.cljs index c07946ba7..4ec8d453d 100644 --- a/frontend/src/uxbox/util/i18n.cljs +++ b/frontend/src/uxbox/util/i18n.cljs @@ -7,72 +7,86 @@ (ns uxbox.util.i18n "A i18n foundation." - (:require [cuerdas.core :as str] - [uxbox.config :as cfg] - [uxbox.util.storage :refer [storage]])) + (:require + [cuerdas.core :as str] + [rumext.alpha :as mf] + [beicon.core :as rx] + [goog.object :as gobj] + [uxbox.config :as cfg] + [uxbox.util.transit :as t] + [uxbox.util.storage :refer [storage]])) -(defonce locale (atom (get storage ::locale cfg/default-language))) -(defonce state (atom {})) +(defonce locale (get storage ::locale cfg/default-language)) +(defonce locale-sub (rx/subject)) +(defonce translations #js {}) -(defn update-locales! - [callback] - (swap! state callback)) +;; The traslations `data` is a javascript object and should be treated +;; with `goog.object` namespace functions instead of a standart +;; clojure functions. This is for performance reasons because this +;; code is executed in the critical part (application bootstrap) and +;; used in many parts of the application. + +(defn init! + [data] + (set! translations data)) (defn set-current-locale! [v] (swap! storage assoc ::locale v) - (reset! locale v)) + (set! locale v) + (rx/push! locale-sub v)) (defn set-default-locale! [] (set-current-locale! cfg/default-language)) -(defn on-locale-change! - [callback] - (add-watch locale ::main (fn [_ _ old-locale new-locale] - (when (not= old-locale new-locale) - (callback new-locale old-locale))))) - -;; A marker type that is used just for mark -;; a parameter that reprsentes the counter. - (deftype C [val] IDeref (-deref [o] val)) -(defn c - [x] - (C. x)) - (defn ^boolean c? [r] (instance? C r)) +(defn- internal-tr + ([locale code] + (let [code (name code) + value (gobj/getValueByKeys translations code locale) + value (if (nil? value) code value)] + (if (array? value) + (aget value 0) + value))) + ([locale code & args] + (let [code (name code) + value (gobj/getValueByKeys translations code locale) + value (if (nil? value) code value) + plural (first (filter c? args)) + value (if (array? value) + (if (= @plural 1) (aget value 0) (aget value 1)) + value)] + (apply str/format value (map #(if (c? %) @% %) args))))) + ;; A main public api for translate strings. +;; A marker type that is used just for mark +;; a parameter that reprsentes the counter. + +(defn c + [x] + (C. x)) + (defn tr - "Translate the string." - ([t] - (let [default (name t) - locale (deref locale) - value (or (get-in @state [locale t]) - default)] - (if (vector? value) - (or (second value) default) - value))) - ([t & args] - (let [locale (deref locale) - value (get-in @state [locale t] (name t)) - plural (first (filter c? args)) - args (mapv #(if (c? %) @% %) args) - value (cond - (and (vector? value) - (= 3 (count value))) - (nth value (min 2 @plural)) + ([code] (internal-tr locale code)) + ([code & args] (apply internal-tr locale code args))) - (vector? value) - (if (= @plural 1) (first value) (second value)) +(defn use-translations + [] + (let [[locale set-locale] (mf/useState locale) + tr-fn (mf/useMemo (fn [] (partial internal-tr locale)) + #js [locale])] + (mf/useEffect (fn [] + (let [sub (rx/sub! locale-sub #(set-locale %))] + #(rx/dispose! sub))) + #js []) + tr-fn)) - :else - value)] - (apply str/format value args)))) diff --git a/frontend/src/uxbox/view.cljs b/frontend/src/uxbox/view.cljs index 247790979..e91cff923 100644 --- a/frontend/src/uxbox/view.cljs +++ b/frontend/src/uxbox/view.cljs @@ -19,17 +19,17 @@ [uxbox.view.ui.lightbox :refer [lightbox]] [uxbox.view.ui.loader :refer [loader]])) -(i18n/update-locales! (fn [locales] - (-> locales - (assoc "en" en/locales) - (assoc "fr" fr/locales)))) +;; (i18n/update-locales! (fn [locales] +;; (-> locales +;; (assoc "en" en/locales) +;; (assoc "fr" fr/locales)))) (declare reinit) -(i18n/on-locale-change! - (fn [new old] - (println "Locale changed from" old " to " new) - (reinit))) +;; (i18n/on-locale-change! +;; (fn [new old] +;; (println "Locale changed from" old " to " new) +;; (reinit))) (defn- on-navigate [router path] diff --git a/frontend/tools.clj b/frontend/tools.clj index 2a542bd10..40605bea3 100644 --- a/frontend/tools.clj +++ b/frontend/tools.clj @@ -1,7 +1,10 @@ -(require '[clojure.pprint :refer [pprint]]) -(require '[clojure.java.shell :as shell]) -(require '[figwheel.main.api :as figwheel]) -(require '[environ.core :refer [env]]) +(require '[clojure.pprint :refer [pprint]] + '[clojure.java.shell :as shell] + '[clojure.java.io :as io] + '[figwheel.main.api :as figwheel] + '[environ.core :refer [env]] + '[jsonista.core :as json] + '[cognitect.transit :as t]) (require '[cljs.build.api :as api] '[cljs.repl :as repl] '[cljs.repl.node :as node]) @@ -12,6 +15,8 @@ '[rebel-readline.cljs.service.local] '[rebel-readline.cljs.repl]) +(import 'java.io.ByteArrayOutputStream) + (defmulti task first) (defmethod task :default @@ -39,7 +44,7 @@ :closure-defines closure-defines :optimizations :none :infer-externs true - :verbose true + :verbose false :source-map true :static-fns false :pretty-print true @@ -153,7 +158,7 @@ (api/build (api/inputs "src" "test") (assoc default-build-options :main 'uxbox.tests.main - :verbose true + :verbose false :target :nodejs :source-map true :output-to "target/tests/main.js" diff --git a/frontend/translations.clj b/frontend/translations.clj new file mode 100644 index 000000000..be5db53ef --- /dev/null +++ b/frontend/translations.clj @@ -0,0 +1,178 @@ +(require '[clojure.pprint :as pp :refer [pprint]]) +(require '[clojure.java.shell :as shell]) +(require '[environ.core :refer [env]]) + +(require '[clojure.walk :as walk] + '[clojure.edn :as edn] + '[clojure.set :as set]) + +(require '[datoteka.core :as fs] + '[jsonista.core :as json]) +(require '[clojure.java.io :as io] + '[clojure.tools.reader :as r] + '[clojure.tools.reader.reader-types :as rt]) + +(import 'java.nio.file.Paths + 'java.nio.file.Path + 'java.nio.file.Files + 'java.nio.file.SimpleFileVisitor + 'java.nio.file.FileVisitResult) + +(extend-protocol io/Coercions + Path + (as-file [it] (.toFile it)) + (as-url [it] (io/as-url (.toFile it)))) + +(defmulti task first) + +(defn- find-translations-in-form + [env form] + (->> form + (walk/postwalk + (fn [fm] + (when (and (list? fm) + (= (first fm) 'tr) + (string? (second fm))) + (let [m (meta (first fm))] + (swap! env conj {:code (second fm) + :file (:file m) + :line (:line m)}))) + fm)))) + +(defn- find-translations-in-file + [env file] + (let [rdr (-> (io/as-file file) + (io/reader) + (rt/source-logging-push-back-reader 1 file))] + (try + (binding [r/*default-data-reader-fn* (constantly nil) + r/*alias-map* {'dw (create-ns 'user) + 'fm (create-ns 'user) + 'us (create-ns 'user) + 'dp (create-ns 'user) + 'cp (create-ns 'user)}] + (loop [] + (let [form (r/read {:eof ::end} rdr)] + (when (not= ::end form) + (find-translations-in-form env form) + (recur))))) + (catch Exception e + ;; (.printStackTrace e) + (println (str "ERROR: on procesing " file "; ignoring...")))))) + +(defn- find-translations-in-directory + [env file] + (->> (proxy [SimpleFileVisitor] [] + (visitFile [path attrs] + (when (= (fs/ext path) "cljs") + (find-translations-in-file env path)) + FileVisitResult/CONTINUE) + (postVisitDirectory [dir exc] + FileVisitResult/CONTINUE)) + (Files/walkFileTree (fs/path file)))) + +(defn- collect-translations + [path] + (let [env (atom [])] + (find-translations-in-directory env path) + @env)) + +(defn- read-json-file + [path] + (when (fs/regular-file? path) + (let [content (json/read-value (slurp (io/as-file path)))] + (into (sorted-map) content)))) + +(defn- read-edn-file + [path] + (when (fs/regular-file? path) + (let [content (edn/read-string (slurp (io/as-file path)))] + (into (sorted-map) content)))) + + +(defn- add-translation + [data {:keys [code file line] :as translation}] + (let [rpath (str file ":" line)] + (if (contains? data code) + (update data code (fn [state] + (if (get state "permanent") + state + (-> state + (dissoc "unused") + (assoc "used-in" (->> (get state "used-in" []) + (remove #(= rpath %)) + (into [rpath]))))))) + (assoc data code {"translations" {"en" nil "fr" nil} + "used-in" [rpath]})))) + +(defn- clean-removed-translations + [data imported] + (let [existing (into #{} (keys data)) + toremove (set/difference existing imported)] + (reduce (fn [data code] + (if (get-in data [code "permanent"]) + data + (-> data + (update code dissoc "used-in") + (update code assoc "unused" true)))) + data + toremove))) + +(defn- ensure-translations-format + [data] + (reduce-kv (fn [data k v] + (if (string? v) + (assoc data k {:translations {:en v}}) + data)) + data + data)) + +(defn- synchronize-translations + [data translations] + (loop [data (ensure-translations-format data) + imported #{} + c (first translations) + r (rest translations)] + (if (nil? c) + (clean-removed-translations data imported) + (recur (add-translation data c) + (conj imported (:code c)) + (first r) + (rest r))))) + +(defn- synchronize-legacy-translations + [data legacy-data lang] + (reduce-kv (fn [data k v] + (if (contains? data k) + (update-in data [k "translations"] assoc lang v) + data)) + data + legacy-data)) + +(defn- write-result! + [data output-path] + (binding [*out* (io/writer (fs/path output-path))] + (let [mapper (json/object-mapper {:pretty true})] + (println (json/write-value-as-string data mapper)) + (flush)))) + +(defn- update-translations + [{:keys [find-directory output-path] :as props}] + (let [data (read-json-file output-path) + translations (collect-translations find-directory) + data (synchronize-translations data translations)] + (write-result! data output-path))) + +(defmethod task "collectmessages" + [[_ in-path out-path]] + (update-translations {:find-directory in-path + :output-path out-path})) + +(defmethod task "merge-with-legacy" + [[_ path lang legacy-path]] + (let [ldata (read-edn-file legacy-path) + data (read-json-file path) + data (synchronize-legacy-translations data ldata lang)] + (write-result! data path))) + +(task *command-line-args*)