(require '[clojure.pprint :as pp :refer [pprint]]) (require '[clojure.edn :as edn] '[clojure.set :as set] '[clojure.java.io :as io]) (require '[datoteka.core :as fs] '[jsonista.core :as json] '[parcera.core :as pa]) (import 'java.nio.file.Paths 'java.nio.file.Path 'java.nio.file.Files 'java.nio.file.SimpleFileVisitor 'java.nio.file.FileVisitResult 'com.fasterxml.jackson.databind.ObjectMapper 'com.fasterxml.jackson.databind.SerializationFeature) (defmulti task first) (defn- find-translations-in-form [form] (reduce (fn [messages node] (let [found (->> node (filter #(and (seq? %) (= :string (first %)))) (map (fn [item] (let [mdata (meta item)] {:code (edn/read-string (second item)) :line (get-in mdata [::pa/start :row])}))))] (into messages found))) [] (->> (tree-seq seq? seq form) (filter #(and (seq? %) (seq? (second %)) (= :list (first %)) (= :symbol (first (second %))) (or (= "t" (second (second %))) (= "tr" (second (second %))))))))) (defn- find-translations [path] (let [forms (pa/ast (slurp path)) spath (str path)] (->> forms (filter #(and (seq? %) (= :list (first %)))) (reduce (fn [messages form] (->> (find-translations-in-form form) (map #(assoc % :file spath)) (into messages))) [])))) (defn- collect-translations [path] (let [messages (atom [])] (->> (proxy [SimpleFileVisitor] [] (visitFile [path attrs] (when (= (fs/ext path) "cljs") (swap! messages into (find-translations path))) FileVisitResult/CONTINUE) (postVisitDirectory [dir exc] FileVisitResult/CONTINUE)) (Files/walkFileTree (fs/path path))) @messages)) (defn- read-json-file [path] (when (fs/regular-file? path) (let [content (json/read-value (io/as-file path))] (reduce-kv (fn [res k v] (let [v (into (sorted-map) v) v (update v "translations" #(into (sorted-map) %))] (assoc res k v))) (sorted-map) content)))) (defn- add-translation [data {:keys [code file line] :as translation}] (let [rpath (str file)] (if (contains? data code) (update data code (fn [state] (if (get state "permanent") state (-> state (dissoc "unused") (update "used-in" conj rpath))))) (assoc data code {"translations" (sorted-map "en" nil "es" 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- initial-cleanup [data] (reduce-kv (fn [data k v] (if (string? v) (assoc data k {"used-in" [] "translations" {:en v}}) (update data k assoc "used-in" []))) data data)) (defn- synchronize-translations [data translations] (loop [data (initial-cleanup 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- write-result! [data output-path] (binding [*out* (io/writer (fs/path output-path))] (let [mapper (doto (ObjectMapper.) (.enable SerializationFeature/ORDER_MAP_ENTRIES_BY_KEYS)) mapper (json/object-mapper {:pretty true :mapper mapper})] (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 "collect" [[_ in-path out-path]] (update-translations {:find-directory in-path :output-path out-path})) (task *command-line-args*)