Compare commits

...

1 Commits

Author SHA1 Message Date
John Swanson
4abe0e0564 Localize more things 2025-10-08 12:07:34 -07:00
13 changed files with 451 additions and 98 deletions

View File

@@ -5,6 +5,7 @@
out either the string literal or a call to `str` concatenating several string literals. Main function will write the
pot file and exit with 0 if all forms could be analyzed else returns 1."
(:require
[clj-yaml.core :as yaml]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[clojure.string :as str]
@@ -103,6 +104,16 @@
(form->messages result))))
(g/grasp roots ::translate)))
(def ^:private sample-content-translations
(->> (slurp (io/resource "unsafe-translations.yml"))
(yaml/parse-string)
(mapcat (fn [{:keys [file messages]}]
(map (fn [message] {:file file :message message}) messages)))))
(def ^:private trash-translations
[{:file "resources/migrations/001_update_migrations.yaml"
:message "Trash"}])
(defn- group-results-by-string
"Each string can be in the pot file once and only once (a string can only have a single translation). Want all
filenames collapsed into a list for each message."
@@ -162,7 +173,10 @@
"String sources and an output filename. Writes the pot file of translation strings found in sources and returns a map
of number of valid usages, number of distinct translation strings, and the bad forms that could not be identified."
[sources filename]
(let [analyzed (analyze-translations sources)
(let [analyzed (concat
(analyze-translations sources)
sample-content-translations
trash-translations)
valid? (comp string? :message)
bad-forms (remove valid? analyzed)
good-forms (filter valid? analyzed)

View File

@@ -1902,6 +1902,73 @@ databaseChangeLog:
path: instance_analytics_views/view_log/v2/mysql-view_log.sql
relativeToChangelogFile: true
- changeSet:
id: v57.2025-10-07T00:00:00
author: johnswanson
comment: Add should_localize column to collection table for automatic localization of sample content
changes:
- addColumn:
tableName: collection
columns:
- column:
name: should_localize
type: ${boolean.type}
remarks: true if this collection's name and description should be automatically translated to the user's locale
defaultValueBoolean: false
constraints:
nullable: false
- changeSet:
id: v57.2025-10-07T00:00:01
author: johnswanson
comment: Add should_localize column to report_card table for automatic localization of sample content
changes:
- addColumn:
tableName: report_card
columns:
- column:
name: should_localize
type: ${boolean.type}
remarks: true if this card's name and description should be automatically translated to the user's locale
defaultValueBoolean: false
constraints:
nullable: false
- changeSet:
id: v57.2025-10-07T00:00:02
author: johnswanson
comment: Add should_localize column to report_dashboard table for automatic localization of sample content
changes:
- addColumn:
tableName: report_dashboard
columns:
- column:
name: should_localize
type: ${boolean.type}
remarks: true if this dashboard's name and description should be automatically translated to the user's locale
defaultValueBoolean: false
constraints:
nullable: false
- changeSet:
id: v57.2025-10-07T00:00:03
author: johnswanson
comment: Set should_localize to true for the Trash collection
changes:
- sql:
sql: UPDATE collection SET should_localize = TRUE WHERE type = 'trash'
# This migration should be changed to a no-op and a new migration inserted that does the load from edn whenever the
# sample content is updated, so that it matches when the version that the sample content was updated from. See
# metabase#50829 for an example where the content was updated.
- changeSet:
id: v57.2025-10-08T10:38:53
author: johnswanson
comment: Create New and improved Sample Content
changes:
- customChange:
class: "metabase.app_db.custom_migrations.CreateSampleContentV3"
# >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<<
########################################################################################################################

View File

@@ -54,7 +54,8 @@
:auto_apply_filters true,
:created_at #t "2024-11-22T15:24:50.968098Z",
:public_uuid nil,
:points_of_interest nil}),
:points_of_interest nil,
:should_localize true}),
:data_permissions
({:id 1,
:group_id 1,
@@ -3288,7 +3289,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:07:01.967450Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Revenue in the US broken down by state",
:archived false,
@@ -3324,7 +3326,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:08:46.497605Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Shows the distribution of the product categories along the scale of customer ratings",
:archived false,
@@ -3360,7 +3363,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:04:29.668182Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Shows a distribution of orders broken down by product categories across our customers' age groups",
:archived false,
@@ -3396,7 +3400,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:18:03.350204Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Feedback on our products via weekly survey",
:archived false,
@@ -3432,7 +3437,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T18:09:20.510367Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Flow from viewing our website (empty) to checkout and subscribe",
:archived false,
@@ -3468,7 +3474,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T18:15:30.282821Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Orders for each product, grouped by product category",
:archived false,
@@ -3504,7 +3511,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T17:12:40.907417Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Analysis of discounts given vs. the size of the order",
:archived false,
@@ -3540,7 +3548,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T17:28:38.671406Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Canonical metric for revenue across all product lines",
:archived false,
@@ -3576,7 +3585,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T19:11:33.837313Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Orders placed per quarter broken down by source and formatted to highlight best and worst quarters",
:archived false,
@@ -3612,7 +3622,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:45:05.402208Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Sankey flow from visiting our website to taking an action",
:archived false,
@@ -3648,7 +3659,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T18:44:18.883535Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Cumulative revenue overlaid with number of orders placed each month",
:archived false,
@@ -3684,7 +3696,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:40:20.256168Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Monthly revenue broken down by products",
:archived false,
@@ -3720,7 +3733,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:48:05.819360Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Number of seats in an average subscription, showing increase and decrease",
:archived false,
@@ -3756,7 +3770,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T18:52:12.837319Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Total revenue last quarter compared to the previous",
:archived false,
@@ -3792,7 +3807,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:27:51.638392Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Customers that signed up for our monthly subscription",
:archived false,
@@ -3828,7 +3844,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T18:23:12.772461Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Compares total revenue this quarter to our goal",
:archived false,
@@ -3864,7 +3881,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:36:31.525367Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description
"Shows a distribution of orders broken down by product category across our customers' individual age values",
@@ -3901,7 +3919,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:19:19.328213Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Canonical metric for number of orders placed",
:archived false,
@@ -3937,7 +3956,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-23T18:50:29.861577Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Breaks down the overall performance of each of the product categories",
:archived false,
@@ -3973,7 +3993,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:00:01.293215Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "An ordered list of our most successful products",
:archived false,
@@ -4009,7 +4030,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:52:15.745463Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Indicates the average customer review of our products",
:archived false,
@@ -4045,7 +4067,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:06:33.226176Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "The most recent subscription in our database",
:archived false,
@@ -4081,7 +4104,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T18:24:57.632676Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Compares the orders of each category quarter over quarter",
:archived false,
@@ -4117,7 +4141,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:58:26.275694Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "More complete look at all recent subscriptions",
:archived false,
@@ -4153,7 +4178,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T19:02:24.506012Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Compares the total number of orders placed for this product this month with the previous period",
:archived false,
@@ -4188,7 +4214,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:55:36.665982Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Total sum of products sold last quarter compared to the previous",
:archived false,
@@ -4224,7 +4251,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:30:41.928984Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Shows the revenue distributed by age group",
:archived false,
@@ -4260,7 +4288,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:15:06.512570Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Compares the total number of orders placed for this product this month with the previous period",
:archived false,
@@ -4295,7 +4324,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:56:30.151532Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Unique customer email addresses last quarter compared to the previous",
:archived false,
@@ -4330,7 +4360,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:32:53.345123Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Flow from viewing our website (empty) to checkout and subscribe",
:archived false,
@@ -4366,7 +4397,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-25T11:11:52.227936Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description nil,
:archived false,
@@ -4402,7 +4434,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:10:25.789381Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Shows a distribution of revenue per individual age values",
:archived false,
@@ -4438,7 +4471,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:16:04.771512Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Total amount of discounts last quarter compared to the previous",
:archived false,
@@ -4473,7 +4507,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:29:19.935683Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Shows a distribution of orders broken down by source across our customers' individual age values",
:archived false,
@@ -4509,7 +4544,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:22:31.846447Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Shows a distribution of orders broken down by source across our customers' age groups",
:archived false,
@@ -4545,7 +4581,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T16:20:40.010971Z",
:public_uuid nil}
:public_uuid nil
:should_localize true}
{:cache_invalidated_at nil,
:description "Compares the total number of orders placed for this product this month with the previous period",
:archived false,
@@ -4580,7 +4617,8 @@
:metabase_version nil,
:parameters "[]",
:created_at #t "2024-11-22T15:54:17.117758Z",
:public_uuid nil}),
:public_uuid nil
:should_localize true}),
:dashboardcard_series (),
:collection
({:authority_level nil,
@@ -4597,7 +4635,8 @@
:entity_id "HyB3nRtqb7pBPhFG26evI",
:location "/",
:namespace nil,
:created_at #t "2024-11-22T15:05:22.685911Z"}
:created_at #t "2024-11-22T15:05:22.685911Z",
:should_localize true}
{:authority_level nil,
:description "Questions and metrics used for the parent dashboard",
:archived true,
@@ -4612,7 +4651,8 @@
:entity_id "54O0IbgCzm290Nnxf1wRH",
:location "/2/",
:namespace nil,
:created_at #t "2024-12-02T19:06:53.806147Z"}),
:created_at #t "2024-12-02T19:06:53.806147Z",
:should_localize true}),
:parameter_card ()
:dimension
({:id 1,

View File

@@ -0,0 +1,82 @@
# IMPORTANT: DO NOT REMOVE ITEMS FROM THIS LIST
- file: "resources/sample-content.edn"
messages:
- "E-commerce Insights"
- "Overview of sample data and hypothetical sales"
- "Orders + People"
- "Sample orders joined with products"
- "Revenue by state"
- "Revenue in the US broken down by state"
- "Customer satisfaction per category"
- "Shows the distribution of the product categories along the scale of customer ratings"
- "Product category orders per age group"
- "Shows a distribution of orders broken down by product categories across our customers' age groups"
- "Customer survey responses"
- "Feedback on our products via weekly survey"
- "Checkout funnel"
- "Flow from viewing our website (empty) to checkout and subscribe"
- "Product breakdown"
- "Orders for each product, grouped by product category"
- "Total order amount vs. discount given"
- "Analysis of discounts given vs. the size of the order"
- "Revenue"
- "Canonical metric for revenue across all product lines"
- "Orders according to sources per quarter"
- "Orders placed per quarter broken down by source and formatted to highlight best and worst quarters"
- "User flow diagram"
- "Sankey flow from visiting our website to taking an action"
- "Revenue and orders over time"
- "Cumulative revenue overlaid with number of orders placed each month"
- "Revenue by product category"
- "Monthly revenue broken down by products"
- "Subscription seats over time"
- "Number of seats in an average subscription, showing increase and decrease"
- "Revenue per quarter"
- "Total revenue last quarter compared to the previous"
- "Number of subscriptions"
- "Customers that signed up for our monthly subscription"
- "Revenue goal for this quarter"
- "Compares total revenue this quarter to our goal"
- "Product category orders per age"
- "Shows a distribution of orders broken down by product category across our customers' individual age values"
- "Number of Orders"
- "Canonical metric for number of orders placed"
- "Total orders by product category"
- "Breaks down the overall performance of each of the product categories"
- "Best selling products"
- "An ordered list of our most successful products"
- "Average product rating"
- "Indicates the average customer review of our products"
- "Most recent subscription"
- "The most recent subscription in our database"
- "Orders by product category"
- "Compares the orders of each category quarter over quarter"
- "All subscriptions in table view"
- "More complete look at all recent subscriptions"
- "Heavy-Duty Silk Chair trend"
- "Compares the total number of orders placed for this product this month with the previous period"
- "Quantity sold per quarter"
- "Total sum of products sold last quarter compared to the previous"
- "Revenue per age group"
- "Shows the revenue distributed by age group"
- "Enormous Wool Car trend"
- "Compares the total number of orders placed for this product this month with the previous period"
- "Unique customers per month"
- "Unique customer email addresses last quarter compared to the previous"
- "Checkout funnel - Modified"
- "Flow from viewing our website (empty) to checkout and subscribe"
- "People with age"
- "Revenue per individual age"
- "Shows a distribution of revenue per individual age values"
- "Discounts given per quarter"
- "Total amount of discounts last quarter compared to the previous"
- "Orders by source per individual age"
- "Shows a distribution of orders broken down by source across our customers' individual age values"
- "Orders by source per age group"
- "Shows a distribution of orders broken down by source across our customers' age groups"
- "Aerodynamic Copper Knife trend"
- "Compares the total number of orders placed for this product this month with the previous period"
- "Examples"
- "Some examples to get started with"
- "E-commerce"
- "Questions and metrics used for the parent dashboard"

View File

@@ -1199,50 +1199,53 @@
(update-vals replace-temporals)
(dissoc :id))))))
(define-migration CreateSampleContentV2
(define-migration CreateSampleContentV2)
;; Does nothing. This is left in so we do not alter the liquibase migration history. See: [[CreateSampleContentV2]].
(define-migration CreateSampleContentV3
;; Adds sample content to a fresh install. Adds curate permissions to the collection for the 'All Users' group.
(when *create-sample-content*
(when (and (config/load-sample-content?)
(not (config/config-bool :mb-enable-test-endpoints)) ; skip sample content for e2e tests to avoid coupling the tests to the contents
(no-user?)
(no-db?))
(let [table-name->raw-rows (load-edn "sample-content.edn")
example-dashboard-id 1
example-collection-id 2 ;; trash collection is 1
expected-sample-db-id 1
dbs (table-name->rows table-name->raw-rows :metabase_database)
_ (t2/query {:insert-into :metabase_database :values dbs})
db-ids (set (map :id (t2/query {:select :id :from :metabase_database})))]
;; If that did not succeed in creating the metabase_database rows we could be reusing a database that
;; previously had rows in it even if there are no users. in this rare care we delete the metabase_database rows
;; and do nothing else, to be safe.
(if (not= db-ids #{expected-sample-db-id})
(when (seq db-ids)
(t2/query {:delete-from :metabase_database :where [:in :id db-ids]}))
(do (doseq [table-name [:collection
:metabase_table
:metabase_field
:report_card
:parameter_card
:report_dashboard
:dashboard_tab
:report_dashboardcard
:dashboardcard_series
:permissions_group
:data_permissions
:dimension]]
(when-let [values (seq (table-name->rows table-name->raw-rows table-name))]
(t2/query {:insert-into table-name :values values})))
(let [group-id (:id (t2/query-one {:select :id :from :permissions_group :where [:= :name "All Users"]}))]
(t2/query {:insert-into :permissions
:values [{:object (format "/collection/%s/" example-collection-id)
:group_id group-id
:perm_type "perms/collection-access"
:perm_value "read-and-write"
:collection_id example-collection-id}]}))
(t2/query {:insert-into :setting
:values [{:key "example-dashboard-id"
:value (str example-dashboard-id)}]})))))))
(when (and *create-sample-content*
(config/load-sample-content?)
(not (config/config-bool :mb-enable-test-endpoints)) ; skip sample content for e2e tests to avoid coupling the tests to the contents
(no-user?)
(no-db?))
(let [table-name->raw-rows (load-edn "sample-content.edn")
example-dashboard-id 1
example-collection-id 2 ;; trash collection is 1
expected-sample-db-id 1
dbs (table-name->rows table-name->raw-rows :metabase_database)
_ (t2/query {:insert-into :metabase_database :values dbs})
db-ids (set (map :id (t2/query {:select :id :from :metabase_database})))]
;; If that did not succeed in creating the metabase_database rows we could be reusing a database that
;; previously had rows in it even if there are no users. in this rare care we delete the metabase_database rows
;; and do nothing else, to be safe.
(if (not= db-ids #{expected-sample-db-id})
(when (seq db-ids)
(t2/query {:delete-from :metabase_database :where [:in :id db-ids]}))
(do (doseq [table-name [:collection
:metabase_table
:metabase_field
:report_card
:parameter_card
:report_dashboard
:dashboard_tab
:report_dashboardcard
:dashboardcard_series
:permissions_group
:data_permissions
:dimension]]
(when-let [values (seq (table-name->rows table-name->raw-rows table-name))]
(t2/query {:insert-into table-name :values values})))
(let [group-id (:id (t2/query-one {:select :id :from :permissions_group :where [:= :name "All Users"]}))]
(t2/query {:insert-into :permissions
:values [{:object (format "/collection/%s/" example-collection-id)
:group_id group-id
:perm_type "perms/collection-access"
:perm_value "read-and-write"
:collection_id example-collection-id}]}))
(t2/query {:insert-into :setting
:values [{:key "example-dashboard-id"
:value (str example-dashboard-id)}]}))))))
(comment
;; How to create `resources/sample-content.edn` used in `CreateSampleContent`

View File

@@ -19,6 +19,7 @@
[metabase.eid-translation.core :as eid-translation]
[metabase.events.core :as events]
[metabase.lib-be.metadata.jvm :as lib.metadata.jvm]
[metabase.localization.models.localization :as localization]
[metabase.models.interface :as mi]
[metabase.notification.core :as notification]
[metabase.permissions.core :as perms]
@@ -767,7 +768,7 @@
(dissoc row :personal_owner_id)))]
(for [row (annotate-collections parent-collection rows options)]
(-> (t2/instance :model/Collection row)
collection/maybe-localize-trash-name
localization/maybe-localize
(update :archived api/bit->boolean)
(t2/hydrate :can_write :effective_location :can_restore :can_delete)
(dissoc :collection_position :display :moderated_status :icon

View File

@@ -15,6 +15,7 @@
[metabase.collections.models.collection.root :as collection.root]
[metabase.config.core :as config :refer [*request-id*]]
[metabase.events.core :as events]
[metabase.localization.models.localization :as localization]
[metabase.models.interface :as mi]
[metabase.models.serialization :as serdes]
[metabase.permissions.core :as perms]
@@ -111,20 +112,14 @@
{:namespace mi/transform-keyword
:authority_level mi/transform-keyword})
(defn maybe-localize-trash-name
"If the collection is the Trash, translate the `name`. This is a public function because we can't rely on
`define-after-select` in all circumstances, e.g. when searching or listing collection items (where we do a direct DB
query without `:model/Collection`)."
[collection]
(cond-> collection
(is-trash? collection) (assoc :name (tru "Trash"))))
(t2/define-after-select :model/Collection [collection]
(maybe-localize-trash-name collection))
(defmethod localization/localizable-fields :model/Collection
[_model]
[:name :description])
(doto :model/Collection
(derive :metabase/model)
(derive :hook/entity-id)
(derive :hook/auto-localize)
(derive ::mi/read-policy.full-perms-for-perms-set)
(derive ::mi/write-policy.full-perms-for-perms-set))

View File

@@ -10,6 +10,7 @@
[metabase.dashboards.models.dashboard-card :as dashboard-card]
[metabase.dashboards.models.dashboard-tab :as dashboard-tab]
[metabase.events.core :as events]
[metabase.localization.models.localization :as localization]
[metabase.models.interface :as mi]
[metabase.models.serialization :as serdes]
[metabase.parameters.params :as params]
@@ -40,7 +41,8 @@
(derive :metabase/model)
(derive :perms/use-parent-collection-perms)
(derive :hook/timestamped?)
(derive :hook/entity-id))
(derive :hook/entity-id)
(derive :hook/auto-localize))
(defmethod mi/can-write? :model/Dashboard
([instance]
@@ -69,6 +71,10 @@
{:parameters mi/transform-card-parameters-list
:embedding_params mi/transform-json})
(defmethod localization/localizable-fields :model/Dashboard
[_model]
[:name :description])
(t2/define-before-delete :model/Dashboard
[dashboard]
(let [dashboard-id (u/the-id dashboard)]

View File

@@ -0,0 +1,62 @@
(ns metabase.localization.models.localization
"Infrastructure for automatic localization of sample content using Toucan2 after-select hooks.
Models that want to support automatic localization should:
1. Derive from `:hook/auto-localize`
2. Implement the `localizable-fields` multimethod to specify which fields should be localized
3. Add a `t2/define-after-select` hook that calls `maybe-localize-row`
Content is marked for localization by setting the `should_localize` database column to true."
(:require
[metabase.util.i18n :refer [unsafe-tru]]
[toucan2.core :as t2]))
(defmulti localizable-fields
"Returns a vector of field keywords that should be automatically localized for the given model.
Models should implement this multimethod to specify which fields should be translated when
`should_localize` is true.
Example:
```
(defmethod localization/localizable-fields :model/Collection
[_model]
[:name :description])
```"
{:arglists '([model])}
(fn [model] model))
(defmethod localizable-fields :default
[_model]
[])
(defn maybe-localize-row
"If `row` has `should_localize = true`, translates the fields specified by `localizable-fields` for this model
using `tru` (user locale translation).
Returns the (potentially modified) row.
This function should be called from a model's `after-select` hook:
```
(t2/define-after-select :model/YourModel [row]
(localization/maybe-localize-row :model/YourModel row))
```"
[model row]
(if (and (isa? model :hook/auto-localize) (:should_localize row))
(let [fields-to-localize (localizable-fields model)]
(reduce
(fn [acc field]
(if-let [value (get acc field)]
(assoc acc field (unsafe-tru value))
acc))
row
fields-to-localize))
row))
(defn maybe-localize
[row]
(maybe-localize-row (t2/model row) row))
(t2/define-after-select :hook/auto-localize
[m]
(maybe-localize-row (t2/model m) m))

View File

@@ -27,6 +27,7 @@
[metabase.lib.schema.template-tag :as lib.schema.template-tag]
[metabase.lib.types.isa :as lib.types]
[metabase.lib.util :as lib.util]
[metabase.localization.models.localization :as localization]
[metabase.models.interface :as mi]
[metabase.models.serialization :as serdes]
[metabase.parameters.params :as params]
@@ -109,12 +110,17 @@
:parameter_mappings mi/transform-parameters-list
:type mi/transform-keyword})
(defmethod localization/localizable-fields :model/Card
[_model]
[:name :description])
(doto :model/Card
(derive :metabase/model)
;; You can read/write a Card if you can read/write its parent Collection
(derive :perms/use-parent-collection-perms)
(derive :hook/timestamped?)
(derive :hook/entity-id))
(derive :hook/entity-id)
(derive :hook/auto-localize))
(defmethod mi/can-write? :model/Card
([instance]

View File

@@ -3,6 +3,7 @@
[clojure.string :as str]
[metabase.collections.models.collection :as collection]
[metabase.collections.models.collection.root :as collection.root]
[metabase.localization.models.localization :as localization]
[metabase.models.interface :as mi]
[metabase.permissions.core :as perms]
[metabase.premium-features.core :as premium-features]
@@ -368,7 +369,7 @@
:always
(assoc :type (:collection_type collection))
:always
collection/maybe-localize-trash-name))
(localization/maybe-localize)))
(defn- normalize-result [result]
(let [instance (to-toucan-instance (t2.realize/realize result))]

View File

@@ -164,6 +164,42 @@
(validate-number-of-args format-string-or-str args)
`(UserLocalizedString. ~format-string-or-str ~(vec args) {}))
(defmacro unsafe-deferred-tru
"The same as `deferred-tru`, but does not validate the arguments!
This allows `(unsafe-deferred-tru some-var)`, where `(deferred-tru some-var)` would fail.
If you use this, YOU ARE RESPONSIBLE for guaranteeing that all the possible values of `some-var` are included in the
output enumerated by `bin/build/src/i18n/enumerate.clj`!!!
For example, `i18n.enumerate/sample-content-translations` collects all of the sample content that should be
translated, and a test in `metabase.localization.models.localization-test` verifies that no sample content exists
that isn't included in this enumeration.
Please don't use this unless you are 100% confident that you've done the above correctly! Generally `deferred-tru`
is vastly preferred!"
{:style/indent [:form]}
[str & args]
`(UserLocalizedString. ~str ~(vec args) {}))
(defmacro unsafe-tru
"The same as `tru`, but does not validate the arguments!
This allows `(unsafe-tru some-var)`, where `(tru some-var)` would fail.
If you use this, YOU ARE RESPONSIBLE for guaranteeing that all the possible values of `some-var` are included in the
output enumerated by `bin/build/src/i18n/enumerate.clj`!!!
For example, `i18n.enumerate/sample-content-translations` collects all of the sample content that should be
translated, and a test in `metabase.localization.models.localization-test` verifies that no sample content exists
that isn't included in this enumeration.
Please don't use this unless you are 100% confident that you've done the above correctly! Generally `tru` is vastly
preferred!"
{:style/indent [:form]}
[str & args]
`(str* (unsafe-deferred-tru ~str ~@args)))
(defmacro deferred-trs
"Similar to `trs` but creates a `SiteLocalizedString` instance so that conversion to the correct locale can be
delayed until it is needed. This is needed as the system locale from the JVM can be overridden/changed by a setting.

View File

@@ -0,0 +1,40 @@
(ns metabase.localization.models.localization-test
(:require
[clojure.data :as data]
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.test :refer [deftest is]]
[metabase.localization.models.localization :as localization]
[metabase.util.date-2 :as u.date]
[metabase.util.yaml :as yaml]))
(def table->model
{:report_dashboard :model/Dashboard
:report_card :model/Card
:collection :model/Collection})
(defn strings-requiring-translation
[]
(->> (edn/read-string {:readers {'t u.date/parse}}
(slurp "resources/sample-content.edn"))
(mapcat (fn [[k vs]] (map (fn [v] [k v]) vs)))
(filter (fn [[_k v]] (:should_localize v)))
(mapcat (fn [[k v]]
(let [model (table->model k)]
(vals (select-keys v (localization/localizable-fields model))))))
(filter some?)
(into #{})))
(defn sample-content-translations []
(->> (yaml/from-file (io/resource "unsafe-translations.yml"))
(mapcat (fn [{:keys [file messages]}]
(map (fn [message] message) messages)))
(into #{})))
(deftest all-sample-content-strings-are-marked-unsafe
(let [[only-required
_only-translated
_in-both]
(data/diff (strings-requiring-translation)
(sample-content-translations))]
(is (nil? only-required))))