Explorar o código

Merge pull request #7708 from zhongwencool/i18n-conf

feat: emqx_conf_schema/emqx_plugins/emqx_dashboard I18n conf
zhongwencool %!s(int64=3) %!d(string=hai) anos
pai
achega
d45700865e

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1494 - 1352
apps/emqx_conf/i18n/emqx_conf_schema.conf


+ 70 - 166
apps/emqx_conf/src/emqx_conf_schema.erl

@@ -105,28 +105,27 @@ fields("cluster") ->
        sc(atom(),
           #{ mapping => "ekka.cluster_name"
            , default => emqxcl
-           , desc => "Human-friendly name of the EMQX cluster."
+           , desc => ?DESC(cluster_name)
            , 'readOnly' => true
            })}
     , {"discovery_strategy",
        sc(hoconsc:enum([manual, static, mcast, dns, etcd, k8s]),
           #{ default => manual
-           , desc => "Service discovery method for the cluster nodes."
+           , desc => ?DESC(cluster_discovery_strategy)
            , 'readOnly' => true
            })}
     , {"autoclean",
        sc(emqx_schema:duration(),
           #{ mapping => "ekka.cluster_autoclean"
            , default => "5m"
-           , desc => "Remove disconnected nodes from the cluster after this interval."
+           , desc => ?DESC(cluster_autoclean)
            , 'readOnly' => true
            })}
     , {"autoheal",
        sc(boolean(),
           #{ mapping => "ekka.cluster_autoheal"
            , default => true
-           , desc => "If <code>true</code>, the node will try to heal network partitions
- automatically."
+           , desc => ?DESC(cluster_autoheal)
            , 'readOnly' => true
           })}
     , {"proto_dist",
@@ -134,7 +133,7 @@ fields("cluster") ->
           #{ mapping => "ekka.proto_dist"
            , default => inet_tcp
            , 'readOnly' => true
-           , desc => "The Erlang distribution protocol for the cluster."
+           , desc => ?DESC(cluster_proto_dist)
           })}
     , {"static",
        sc(ref(cluster_static),
@@ -162,7 +161,7 @@ fields(cluster_static) ->
     [ {"seeds",
       sc(hoconsc:array(atom()),
          #{ default => []
-          , desc => "List EMQX node names in the static cluster. See <code>node.name</code>."
+          , desc => ?DESC(cluster_static_seeds)
           , 'readOnly' => true
          })}
     ];
@@ -171,50 +170,49 @@ fields(cluster_mcast) ->
     [ {"addr",
        sc(string(),
           #{ default => "239.192.0.1"
-           , desc => "Multicast IPv4 address."
+           , desc => ?DESC(cluster_mcast_addr)
            , 'readOnly' => true
           })}
     , {"ports",
        sc(hoconsc:array(integer()),
           #{ default => [4369, 4370]
            , 'readOnly' => true
-           , desc => "List of UDP ports used for service discovery.<br/>
-Note: probe messages are broadcast to all the specified ports."
+           , desc => ?DESC(cluster_mcast_ports)
            })}
     , {"iface",
        sc(string(),
           #{ default => "0.0.0.0"
-           , desc => "Local IP address the node discovery service needs to bind to."
+           , desc => ?DESC(cluster_mcast_iface)
            , 'readOnly' => true
           })}
     , {"ttl",
        sc(range(0, 255),
           #{ default => 255
-           , desc => "Time-to-live (TTL) for the outgoing UDP datagrams."
+           , desc => ?DESC(cluster_mcast_ttl)
            , 'readOnly' => true
           })}
     , {"loop",
        sc(boolean(),
           #{ default => true
-           , desc => "If <code>true</code>, loop UDP datagrams back to the local socket."
+           , desc => ?DESC(cluster_mcast_loop)
            , 'readOnly' => true
           })}
     , {"sndbuf",
        sc(emqx_schema:bytesize(),
           #{ default => "16KB"
-           , desc => "Size of the kernel-level buffer for outgoing datagrams."
+           , desc => ?DESC(cluster_mcast_sndbuf)
            , 'readOnly' => true
           })}
     , {"recbuf",
        sc(emqx_schema:bytesize(),
           #{ default => "16KB"
-           , desc => "Size of the kernel-level buffer for incoming datagrams."
+           , desc => ?DESC(cluster_mcast_recbuf)
            , 'readOnly' => true
           })}
     , {"buffer",
        sc(emqx_schema:bytesize(),
           #{ default =>"32KB"
-           , desc => "Size of the user-level buffer."
+           , desc => ?DESC(cluster_mcast_buffer)
            , 'readOnly' => true
           })}
     ];
@@ -223,13 +221,13 @@ fields(cluster_dns) ->
     [ {"name",
        sc(string(),
           #{ default => "localhost"
-           , desc => "The domain name of the EMQX cluster."
+           , desc => ?DESC(cluster_dns_name)
            , 'readOnly' => true
           })}
     , {"app",
        sc(string(),
           #{ default => "emqx"
-           , desc => "The symbolic name of the EMQX service."
+           , desc => ?DESC(cluster_dns_app)
            , 'readOnly' => true
            })}
     ];
@@ -237,25 +235,24 @@ fields(cluster_dns) ->
 fields(cluster_etcd) ->
     [ {"server",
        sc(emqx_schema:comma_separated_list(),
-          #{ desc => "List of endpoint URLs of the etcd cluster"
+          #{ desc => ?DESC(cluster_etcd_server)
            , 'readOnly' => true
            })}
     , {"prefix",
        sc(string(),
           #{ default => "emqxcl"
-           , desc => "Key prefix used for EMQX service discovery."
+           , desc => ?DESC(cluster_etcd_prefix)
            , 'readOnly' => true
            })}
     , {"node_ttl",
        sc(emqx_schema:duration(),
           #{ default => "1m"
            , 'readOnly' => true
-           , desc => "Expiration time of the etcd key associated with the node.
-It is refreshed automatically, as long as the node is alive."
+           , desc => ?DESC(cluster_etcd_node_ttl)
            })}
     , {"ssl",
        sc(hoconsc:ref(emqx_schema, ssl_client_opts),
-          #{ desc => "Options for the TLS connection to the etcd cluster."
+          #{ desc => ?DESC(cluster_etcd_ssl)
            , 'readOnly' => true
            })}
     ];
@@ -263,42 +260,37 @@ It is refreshed automatically, as long as the node is alive."
 fields(cluster_k8s) ->
     [ {"apiserver",
        sc(string(),
-          #{ desc => "Kubernetes API endpoint URL."
+          #{ desc => ?DESC(cluster_k8s_apiserver)
            , 'readOnly' => true
            })}
     , {"service_name",
        sc(string(),
           #{ default => "emqx"
-           , desc => "EMQX broker service name."
+           , desc => ?DESC(cluster_k8s_service_name)
            , 'readOnly' => true
            })}
     , {"address_type",
        sc(hoconsc:enum([ip, dns, hostname]),
-          #{ desc => "Address type used for connecting to the discovered nodes."
+          #{ desc => ?DESC(cluster_k8s_address_type)
            , 'readOnly' => true
            })}
     , {"app_name",
        sc(string(),
           #{ default => "emqx"
            , 'readOnly' => true
-           , desc => "This parameter should be set to the part of the <code>node.name</code>
-before the '@'.<br/>
-For example, if the <code>node.name</code> is <code>emqx@127.0.0.1</code>, then this parameter
-should be set to <code>emqx</code>."
+           , desc => ?DESC(cluster_k8s_app_name)
            })}
     , {"namespace",
        sc(string(),
           #{ default => "default"
-           , desc => "Kubernetes namespace."
+           , desc => ?DESC(cluster_k8s_namespace)
            , 'readOnly' => true
            })}
     , {"suffix",
        sc(string(),
           #{ default => "pod.local"
            , 'readOnly' => true
-           , desc => "Node name suffix.<br/>
-Note: this parameter is only relevant when <code>address_type</code> is <code>dns</code>
-or <code>hostname</code>."
+           , desc => ?DESC(cluster_k8s_suffix)
            })}
     ];
 
@@ -307,8 +299,7 @@ fields("node") ->
        sc(string(),
           #{ default => "emqx@127.0.0.1"
            , 'readOnly' => true
-           ,  desc => "Unique name of the EMQX node. It must follow <code>%name%@FQDN</code> or
- <code>%name%@IPv4</code> format."
+           ,  desc => ?DESC(node_name)
            })}
     , {"cookie",
        sc(string(),
@@ -316,65 +307,47 @@ fields("node") ->
              default => "emqxsecretcookie",
              'readOnly' => true,
              sensitive => true,
-             desc => "Secret cookie is a random string that should be the same on all nodes in
- the given EMQX cluster, but unique per EMQX cluster. It is used to prevent EMQX nodes that
- belong to different clusters from accidentally connecting to each other."
+             desc => ?DESC(node_cookie)
            })}
     , {"data_dir",
        sc(string(),
           #{ required => true,
              'readOnly' => true,
              mapping => "emqx.data_dir",
-             desc =>
-"""
-Path to the persistent data directory.
-Possible auto-created subdirectories are:
-  - `mnesia/\<node_name>`: EMQX's built-in database directory.
-    For example, `mnesia/emqx@127.0.0.1`.
-    There should be only one such subdirectory.
-    Meaning, in case the node is to be renamed (to e.g. `emqx@10.0.1.1`),
-    the old dir should be deleted first.
-  - `configs`: Generated configs at boot time, and cluster/local override configs.
-  - `patches`: Hot-patch beam files are to be placed here.
-  - `trace`: Trace log files.
-
-**NOTE**: One data dir cannot be shared by two or more EMQX nodes.
-"""
+             desc => ?DESC(node_data_dir)
            })}
     , {"config_files",
        sc(list(string()),
           #{ mapping => "emqx.config_files"
            , default => undefined
            , 'readOnly' => true
-           , desc => "List of configuration files that are read during startup. The order is
- significant: later configuration files override the previous ones."
+           , desc => ?DESC(node_config_files)
            })}
     , {"global_gc_interval",
        sc(emqx_schema:duration(),
          #{ mapping => "emqx_machine.global_gc_interval"
           , default => "15m"
-          , desc => "Periodic garbage collection interval."
+          , desc => ?DESC(node_global_gc_interval)
           , 'readOnly' => true
           })}
     , {"crash_dump_file",
        sc(file(),
           #{ mapping => "vm_args.-env ERL_CRASH_DUMP"
-           , desc => "Location of the crash dump file"
+           , desc => ?DESC(node_crash_dump_file)
            , 'readOnly' => true
            })}
     , {"crash_dump_seconds",
        sc(emqx_schema:duration_s(),
           #{ mapping => "vm_args.-env ERL_CRASH_DUMP_SECONDS"
            , default => "30s"
-           , desc => "The number of seconds that the broker is allowed to spend writing
-a crash dump"
+           , desc => ?DESC(node_crash_dump_seconds)
            , 'readOnly' => true
            })}
     , {"crash_dump_bytes",
        sc(emqx_schema:bytesize(),
           #{ mapping => "vm_args.-env ERL_CRASH_DUMP_BYTES"
            , default => "100MB"
-           , desc => "The maximum size of a crash dump file in bytes."
+           , desc => ?DESC(node_crash_dump_bytes)
            , 'readOnly' => true
            })}
     , {"dist_net_ticktime",
@@ -382,28 +355,25 @@ a crash dump"
           #{ mapping => "vm_args.-kernel net_ticktime"
            , default => "2m"
            , 'readOnly' => true
-           , desc => "This is the approximate time an EMQX node may be unresponsive "
-                     "until it is considered down and thereby disconnected."
+           , desc => ?DESC(node_dist_net_ticktime)
            })}
     , {"backtrace_depth",
        sc(integer(),
           #{ mapping => "emqx_machine.backtrace_depth"
            , default => 23
            , 'readOnly' => true
-           , desc => "Maximum depth of the call stack printed in error messages and
- <code>process_info</code>."
+           , desc => ?DESC(node_backtrace_depth)
            })}
     , {"applications",
        sc(emqx_schema:comma_separated_atoms(),
           #{ mapping => "emqx_machine.applications"
            , default => []
            , 'readOnly' => true
-           , desc => "List of Erlang applications that shall be rebooted when the EMQX broker joins
- the cluster."
+           , desc => ?DESC(node_applications)
            })}
     , {"etc_dir",
        sc(string(),
-          #{ desc => "<code>etc</code> dir for the node"
+          #{ desc => ?DESC(node_etc_dir)
            , 'readOnly' => true
            }
          )}
@@ -420,81 +390,45 @@ fields("db") ->
           #{ mapping => "mria.db_backend"
            , default => rlog
            , 'readOnly' => true
-           , desc => """
-Select the backend for the embedded database.<br/>
-<code>rlog</code> is the default backend,
-that is suitable for very large clusters.<br/>
-<code>mnesia</code> is a backend that offers decent performance in small clusters.
-"""
+           , desc => ?DESC(db_backend)
            })}
     , {"role",
        sc(hoconsc:enum([core, replicant]),
           #{ mapping => "mria.node_role"
            , default => core
            , 'readOnly' => true
-           , desc => """
-Select a node role.<br/>
-<code>core</code> nodes provide durability of the data, and take care of writes.
-It is recommended to place core nodes in different racks or different availability zones.<br/>
-<code>replicant</code> nodes are ephemeral worker nodes. Removing them from the cluster
-doesn't affect database redundancy<br/>
-It is recommended to have more replicant nodes than core nodes.<br/>
-Note: this parameter only takes effect when the <code>backend</code> is set
-to <code>rlog</code>.
-"""
+           , desc => ?DESC(db_role)
            })}
     , {"core_nodes",
        sc(emqx_schema:comma_separated_atoms(),
           #{ mapping => "mria.core_nodes"
            , default => []
            , 'readOnly' => true
-           , desc => """
-List of core nodes that the replicant will connect to.<br/>
-Note: this parameter only takes effect when the <code>backend</code> is set
-to <code>rlog</code> and the <code>role</code> is set to <code>replicant</code>.<br/>
-This value needs to be defined for manual or static cluster discovery mechanisms.<br/>
-If an automatic cluster discovery mechanism is being used (such as <code>etcd</code>),
-there is no need to set this value.
-"""
+           , desc => ?DESC(db_core_nodes)
            })}
     , {"rpc_module",
        sc(hoconsc:enum([gen_rpc, rpc]),
           #{ mapping => "mria.rlog_rpc_module"
            , default => gen_rpc
            , 'readOnly' => true
-           , desc => """
-Protocol used for pushing transaction logs to the replicant nodes.
-"""
+           , desc => ?DESC(db_rpc_module)
            })}
     , {"tlog_push_mode",
        sc(hoconsc:enum([sync, async]),
           #{ mapping => "mria.tlog_push_mode"
            , default => async
            , 'readOnly' => true
-           , desc => """
-In sync mode the core node waits for an ack from the replicant nodes before sending the next
-transaction log entry.
-"""
+           , desc => ?DESC(db_tlog_push_mode)
            })}
     , {"default_shard_transport",
        sc(hoconsc:enum([gen_rpc, distr]),
           #{ mapping => "mria.shard_transport"
            , default => gen_rpc
-           , desc =>
-               "Defines the default transport for pushing transaction logs.<br/>"
-               "This may be overridden on a per-shard basis in <code>db.shard_transports</code>."
-               "<code>gen_rpc</code> uses the <code>gen_rpc</code> library, "
-               "<code>distr</code> uses the Erlang distribution.<br/>"
+           , desc => ?DESC(db_default_shard_transport)
            })}
     , {"shard_transports",
        sc(map(shard, hoconsc:enum([gen_rpc, distr])),
-          #{ desc =>
-               "Allows to tune the transport method used for transaction log replication, "
-               "on a per-shard basis.<br/>"
-               "<code>gen_rpc</code> uses the <code>gen_rpc</code> library, "
-               "<code>distr</code> uses the Erlang distribution.<br/>"
-               "If not specified, the default is to use the value "
-               "set in <code>db.default_shard_transport</code>."
+          #{ desc => ?DESC(db_shard_transports)
            , mapping => "emqx_machine.custom_shard_transports"
            , default => #{}
            })}
@@ -503,19 +437,17 @@ transaction log entry.
 fields("cluster_call") ->
     [ {"retry_interval",
        sc(emqx_schema:duration(),
-         #{ desc => "Time interval to retry after a failed call."
+         #{ desc => ?DESC(cluster_call_retry_interval)
           , default => "1s"
           })}
     , {"max_history",
        sc(range(1, 500),
-          #{ desc => "Retain the maximum number of completed transactions (for queries)."
+          #{ desc => ?DESC(cluster_call_max_history)
            , default => 100
            })}
     , {"cleanup_interval",
        sc(emqx_schema:duration(),
-          #{ desc =>
-"Time interval to clear completed but stale transactions.
-Ensure that the number of completed transactions is less than the <code>max_history</code>."
+          #{ desc => ?DESC(cluster_call_cleanup_interval)
            , default => "5m"
            })}
     ];
@@ -524,136 +456,118 @@ fields("rpc") ->
     [ {"mode",
        sc(hoconsc:enum([sync, async]),
           #{ default => async
-           , desc => "In <code>sync</code> mode the sending side waits for the ack from the "
-                     "receiving side."
+           , desc => ?DESC(rpc_mode)
            })}
     , {"driver",
        sc(hoconsc:enum([tcp, ssl]),
           #{ mapping => "gen_rpc.driver"
            , default => tcp
-           , desc => "Transport protocol used for inter-broker communication"
+           , desc => ?DESC(rpc_driver)
            })}
     , {"async_batch_size",
        sc(integer(),
           #{ mapping => "gen_rpc.max_batch_size"
            , default => 256
-           , desc => "The maximum number of batch messages sent in asynchronous mode. "
-                     "Note that this configuration does not work in synchronous mode."
+           , desc => ?DESC(rpc_async_batch_size)
            })}
     , {"port_discovery",
        sc(hoconsc:enum([manual, stateless]),
           #{ mapping => "gen_rpc.port_discovery"
            , default => stateless
-           , desc => "<code>manual</code>: discover ports by <code>tcp_server_port</code>.<br/>"
-                     "<code>stateless</code>: discover ports in a stateless manner, "
-                     "using the following algorithm. "
-                     "If node name is <code>emqxN@127.0.0.1</code>, where the N is an integer, "
-                     "then the listening port will be 5370 + N."
+           , desc => ?DESC(rpc_port_discovery)
            })}
     , {"tcp_server_port",
        sc(integer(),
           #{ mapping => "gen_rpc.tcp_server_port"
            , default => 5369
-           , desc => "Listening port used by RPC local service.<br/> "
-                     "Note that this config only takes effect when rpc.port_discovery "
-                     "is set to manual."
+           , desc => ?DESC(rpc_tcp_server_port)
            })}
     , {"ssl_server_port",
        sc(integer(),
           #{ mapping => "gen_rpc.ssl_server_port"
            , default => 5369
-           , desc => "Listening port used by RPC local service.<br/> "
-                     "Note that this config only takes effect when rpc.port_discovery "
-                     "is set to manual and <code>driver</code> is set to <code>ssl</code>."
+           , desc => ?DESC(rpc_ssl_server_port)
            })}
     , {"tcp_client_num",
        sc(range(1, 256),
           #{ default => 10
-           , desc => "Set the maximum number of RPC communication channels initiated by this node "
-                     "to each remote node."
+           , desc => ?DESC(rpc_tcp_client_num)
            })}
     , {"connect_timeout",
        sc(emqx_schema:duration(),
           #{ mapping => "gen_rpc.connect_timeout"
            , default => "5s"
-           , desc => "Timeout for establishing an RPC connection."
+           , desc => ?DESC(rpc_connect_timeout)
            })}
     , {"certfile",
        sc(file(),
           #{ mapping => "gen_rpc.certfile"
-           , desc => "Path to TLS certificate file used to validate identity of the cluster nodes. "
-                     "Note that this config only takes effect when <code>rpc.driver</code> "
-                     "is set to <code>ssl</code>."
+           , desc => ?DESC(rpc_certfile)
            })}
     , {"keyfile",
        sc(file(),
           #{ mapping => "gen_rpc.keyfile"
-           , desc => "Path to the private key file for the <code>rpc.certfile</code>.<br/>"
-                     "Note: contents of this file are secret, so "
-                     "it's necessary to set permissions to 600."
+           , desc => ?DESC(rpc_keyfile)
            })}
     , {"cacertfile",
        sc(file(),
           #{ mapping => "gen_rpc.cacertfile"
-           , desc => "Path to certification authority TLS certificate file used to validate "
-                     "<code>rpc.certfile</code>.<br/>"
-                     "Note: certificates of all nodes in the cluster must be signed by the same CA."
+           , desc => ?DESC(rpc_cacertfile)
            })}
     , {"send_timeout",
        sc(emqx_schema:duration(),
           #{ mapping => "gen_rpc.send_timeout"
            , default => "5s"
-           , desc => "Timeout for sending the RPC request."
+           , desc => ?DESC(rpc_send_timeout)
            })}
     , {"authentication_timeout",
        sc(emqx_schema:duration(),
           #{ mapping=> "gen_rpc.authentication_timeout"
            , default => "5s"
-           , desc => "Timeout for the remote node authentication."
+           , desc => ?DESC(rpc_authentication_timeout)
            })}
     , {"call_receive_timeout",
        sc(emqx_schema:duration(),
           #{ mapping => "gen_rpc.call_receive_timeout"
            , default => "15s"
-           , desc => "Timeout for the reply to a synchronous RPC."
+           , desc => ?DESC(rpc_call_receive_timeout)
            })}
     , {"socket_keepalive_idle",
        sc(emqx_schema:duration_s(),
           #{ mapping => "gen_rpc.socket_keepalive_idle"
            , default => "7200s"
-           , desc => "How long the connections between the brokers "
-                     "should remain open after the last message is sent."
+              , desc => ?DESC(rpc_socket_keepalive_idle)
+
            })}
     , {"socket_keepalive_interval",
        sc(emqx_schema:duration_s(),
           #{ mapping => "gen_rpc.socket_keepalive_interval"
            , default => "75s"
-           , desc => "The interval between keepalive messages."
+           , desc => ?DESC(rpc_socket_keepalive_interval)
            })}
     , {"socket_keepalive_count",
        sc(integer(),
           #{ mapping => "gen_rpc.socket_keepalive_count"
            , default => 9
-           , desc => "How many times the keepalive probe message can fail to receive a reply "
-                     "until the RPC connection is considered lost."
+           , desc =>           ?DESC(rpc_socket_keepalive_count)
            })}
     , {"socket_sndbuf",
        sc(emqx_schema:bytesize(),
           #{ mapping => "gen_rpc.socket_sndbuf"
            , default => "1MB"
-           , desc => "TCP tuning parameters. TCP sending buffer size."
+           , desc => ?DESC(rpc_socket_sndbuf)
            })}
     , {"socket_recbuf",
        sc(emqx_schema:bytesize(),
           #{ mapping => "gen_rpc.socket_recbuf"
            , default => "1MB"
-           , desc => "TCP tuning parameters. TCP receiving buffer size."
+           , desc => ?DESC(rpc_socket_recbuf)
            })}
     , {"socket_buffer",
        sc(emqx_schema:bytesize(),
           #{ mapping => "gen_rpc.socket_buffer"
            , default => "1MB"
-           , desc => "TCP tuning parameters. Socket buffer size in user mode."
+           , desc => ?DESC(rpc_socket_buffer)
            })}
     ];
 
@@ -1094,15 +1008,5 @@ emqx_schema_high_prio_roots() ->
     Roots = emqx_schema:roots(high),
     Authz = {"authorization",
              sc(hoconsc:ref(?MODULE, "authorization"),
-             #{ desc => """
-Authorization a.k.a. ACL.<br>
-In EMQX, MQTT client access control is extremely flexible.<br>
-An out-of-the-box set of authorization data sources are supported.
-For example,<br>
-'file' source is to support concise and yet generic ACL rules in a file;<br>
-'built_in_database' source can be used to store per-client customizable rule sets,
-natively in the EMQX node;<br>
-'http' source to make EMQX call an external HTTP API to make the decision;<br>
-'PostgreSQL' etc. to look up clients or rules from external databases;<br>
-""" })},
+             #{ desc => ?DESC(authorization)})},
     lists:keyreplace("authorization", 1, Roots, Authz).

+ 0 - 12
apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf

@@ -1,12 +0,0 @@
-emqx_dashboard_schema {
-  protocol {
-    desc {
-      en: "Protocol Name"
-      zh: "协议名"
-    }
-    label: {
-      en: "Protocol"
-      zh: "协议"
-    }
-  }
-}

+ 190 - 0
apps/emqx_dashboard/i18n/emqx_dashboard_schema.conf

@@ -0,0 +1,190 @@
+emqx_dashboard_schema {
+  listeners {
+    desc {
+      en: """HTTP(s) listeners are identified by their protocol type and are <br>
+used to serve dashboard UI and restful HTTP API.<br>
+Listeners must have a unique combination of port number and IP address.<br>
+For example, an HTTP listener can listen on all configured IP addresses
+on a given port for a machine by specifying the IP address 0.0.0.0.<br>
+Alternatively, the HTTP listener can specify a unique IP address for each listener,
+but use the same port."""
+      zh: """仪表盘监听器设置。"""
+    }
+    label {
+      en: "Listeners"
+      zh: "监听器"
+    }
+  }
+  sample_interval {
+    desc {
+      en: """How often to update metrics displayed in the dashboard.<br/>"
+Note: `sample_interval` should be a divisor of 60."""
+      zh: """更新仪表板中显示的指标的时间间隔。"""
+    }
+  }
+  token_expired_time {
+    desc {
+      en: "JWT token expiration time."
+      zh: "JWT token 过期时间"
+    }
+    label {
+      en: "Token expired time"
+      zh: "JWT 过期时间"
+    }
+  }
+  num_acceptors {
+    desc {
+      en: "Socket acceptor pool size for TCP protocols."
+      zh: "TCP协议的Socket acceptor池大小"
+    }
+    label {
+      en: "Number of acceptors"
+      zh: "Acceptor 数量"
+    }
+  }
+  max_connections {
+    desc {
+      en: "Maximum number of simultaneous connections."
+      zh: "同时处理的最大连接数"
+    }
+    label {
+      en: "Maximum connections"
+      zh: "最大连接数"
+    }
+  }
+  backlog {
+    desc {
+      en: "Defines the maximum length that the queue of pending connections can grow to."
+      zh: "排队等待连接的队列的最大长度"
+    }
+    label {
+      en: "Backlog"
+      zh: "排队长度"
+    }
+  }
+  send_timeout {
+    desc {
+      en: "Send timeout for the socket."
+      zh: "Socket发送超时时间"
+    }
+    label {
+      en: "Send timeout"
+      zh: "发送超时时间"
+    }
+  }
+  inet6 {
+    desc {
+      en: "Enable IPv6 support."
+      zh: "启用IPv6"
+    }
+    label {
+      en: "IPv6"
+      zh: "IPv6"
+    }
+  }
+  ipv6_v6only {
+    desc {
+      en: "Disable IPv4-to-IPv6 mapping for the listener."
+      zh: "禁用IPv4-to-IPv6映射"
+    }
+    label {
+      en: "IPv6 only"
+      zh: "IPv6 only"
+    }
+  }
+  desc_dashboard {
+    desc {
+      en: "Configuration for EMQX dashboard."
+      zh: "EMQX仪表板配置"
+    }
+    label {
+      en: "Dashboard"
+      zh: "仪表板"
+    }
+  }
+  desc_listeners {
+    desc {
+      en: "Configuration for the dashboard listener."
+      zh: "仪表板监听器配置"
+    }
+    label {
+      en: "Listeners"
+      zh: "监听器"
+    }
+  }
+  desc_http {
+    desc {
+      en: "Configuration for the dashboard listener (plaintext)."
+      zh: "仪表板监听器(HTTP)配置"
+    }
+    label {
+      en: "HTTP"
+      zh: "HTTP"
+    }
+  }
+  desc_https {
+    desc {
+      en: "Configuration for the dashboard listener (TLS)."
+      zh: "仪表板监听器(HTTPS)配置"
+    }
+    label {
+      en: "HTTPS"
+      zh: "HTTPS"
+    }
+  }
+  bind {
+    desc {
+      en: "Port without IP(18083) or port with specified IP(127.0.0.1:18083)."
+      zh: "监听的地址与端口"
+    }
+    label {
+      en: "Bind"
+      zh: "绑定端口"
+    }
+  }
+  default_username {
+    desc {
+      en: "The default username of the automatically created dashboard user."
+      zh: "默认的仪表板用户名"
+    }
+    label {
+      en: "Default username"
+      zh: "默认用户名"
+    }
+  }
+  default_password {
+    desc {
+      en: """The initial default password for dashboard 'admin' user.<br>"
+For safety, it should be changed as soon as possible."""
+      zh: """默认的仪表板用户密码<br>
+为了安全,应该尽快修改密码。"""
+    }
+    label {
+      en: "Default password"
+      zh: "默认密码"
+    }
+  }
+  cors {
+    desc {
+      en: """Support Cross-Origin Resource Sharing (CORS).<br>
+Allows a server to indicate any origins (domain, scheme, or port) other than <br
+its own from which a browser should permit loading resources."""
+      zh: """支持跨域资源共享(CORS)<br>
+允许服务器指示任何来源(域名、协议或端口),除了本服务器之外的任何浏览器应允许加载资源。"""
+    }
+    label {
+      en: "CORS"
+      zh: "跨域资源共享"
+    }
+  }
+  i18n_lang {
+    desc {
+      en: "Internationalization language support."
+      zh: "多语言支持"
+    }
+    label {
+      en: "I18n language"
+      zh: "多语言支持"
+    }
+  }
+}

+ 15 - 9
apps/emqx_dashboard/src/emqx_dashboard_monitor.erl

@@ -313,7 +313,7 @@ next_interval() ->
 sample(Time) ->
     Fun =
         fun(Key, Res) ->
-            maps:put(Key, value(Key), Res)
+            maps:put(Key, getstats(Key), Res)
         end,
     Data = lists:foldl(Fun, #{}, ?SAMPLER_LIST),
     #emqx_monit{time = Time, data = Data}.
@@ -362,11 +362,17 @@ count_map(M1, M2) ->
         end,
     lists:foldl(Fun, #{}, ?SAMPLER_LIST).
 
-value(connections) -> emqx_stats:getstat('connections.count');
-value(topics) -> emqx_stats:getstat('topics.count');
-value(subscriptions) -> emqx_stats:getstat('subscriptions.count');
-value(received) -> emqx_metrics:val('messages.received');
-value(received_bytes) -> emqx_metrics:val('bytes.received');
-value(sent) -> emqx_metrics:val('messages.sent');
-value(sent_bytes) -> emqx_metrics:val('bytes.sent');
-value(dropped) -> emqx_metrics:val('messages.dropped').
+getstats(Key) ->
+    %% Stats ets maybe not exist when ekka join.
+    try stats(Key)
+    catch _: _ -> 0
+    end.
+
+stats(connections) -> emqx_stats:getstat('connections.count');
+stats(topics) -> emqx_stats:getstat('topics.count');
+stats(subscriptions) -> emqx_stats:getstat('subscriptions.count');
+stats(received) -> emqx_metrics:val('messages.received');
+stats(received_bytes) -> emqx_metrics:val('bytes.received');
+stats(sent) -> emqx_metrics:val('messages.sent');
+stats(sent_bytes) -> emqx_metrics:val('bytes.sent');
+stats(dropped) -> emqx_metrics:val('messages.dropped').

+ 26 - 42
apps/emqx_dashboard/src/emqx_dashboard_schema.erl

@@ -32,16 +32,7 @@ fields("dashboard") ->
         {listeners,
             sc(
                 ref("listeners"),
-                #{
-                    desc =>
-                        "HTTP(s) listeners are identified by their protocol type and are\n"
-                        "used to serve dashboard UI and restful HTTP API.<br>\n"
-                        "Listeners must have a unique combination of port number and IP address.<br>\n"
-                        "For example, an HTTP listener can listen on all configured IP addresses\n"
-                        "on a given port for a machine by specifying the IP address 0.0.0.0.<br>\n"
-                        "Alternatively, the HTTP listener can specify a unique IP address for each listener,\n"
-                        "but use the same port."
-                }
+                #{ desc => ?DESC(listeners)}
             )},
         {default_username, fun default_username/1},
         {default_password, fun default_password/1},
@@ -50,9 +41,8 @@ fields("dashboard") ->
                 emqx_schema:duration_s(),
                 #{
                     default => "10s",
-                    desc =>
-                        "How often to update metrics displayed in the dashboard.<br/>"
-                        "Note: `sample_interval` should be a divisor of 60."
+                    desc => ?DESC(sample_interval),
+                    validator => fun validate_sample_interval/1
                 }
             )},
         {token_expired_time,
@@ -60,7 +50,7 @@ fields("dashboard") ->
                 emqx_schema:duration(),
                 #{
                     default => "30m",
-                    desc => "JWT token expiration time."
+                    desc => ?DESC(token_expired_time)
                 }
             )},
         {cors, fun cors/1},
@@ -93,7 +83,7 @@ fields("http") ->
                 integer(),
                 #{
                     default => 4,
-                    desc => "Socket acceptor pool size for TCP protocols."
+                    desc => ?DESC(num_acceptors)
                 }
             )},
         {"max_connections",
@@ -101,7 +91,7 @@ fields("http") ->
                 integer(),
                 #{
                     default => 512,
-                    desc => "Maximum number of simultaneous connections."
+                    desc => ?DESC(max_connections)
                 }
             )},
         {"backlog",
@@ -109,8 +99,7 @@ fields("http") ->
                 integer(),
                 #{
                     default => 1024,
-                    desc =>
-                        "Defines the maximum length that the queue of pending connections can grow to."
+                    desc => ?DESC(backlog)
                 }
             )},
         {"send_timeout",
@@ -118,7 +107,7 @@ fields("http") ->
                 emqx_schema:duration(),
                 #{
                     default => "5s",
-                    desc => "Send timeout for the socket."
+                    desc => ?DESC(send_timeout)
                 }
             )},
         {"inet6",
@@ -126,7 +115,7 @@ fields("http") ->
                 boolean(),
                 #{
                     default => false,
-                    desc => "Sets up the listener for IPv6."
+                    desc => ?DESC(inet6)
                 }
             )},
         {"ipv6_v6only",
@@ -134,7 +123,7 @@ fields("http") ->
                 boolean(),
                 #{
                     default => false,
-                    desc => "Disable IPv4-to-IPv6 mapping for the listener."
+                    desc => ?DESC(ipv6_v6only)
                 }
             )}
     ];
@@ -145,27 +134,22 @@ fields("https") ->
             emqx_schema:server_ssl_opts_schema(#{}, true)
         ).
 
-desc("dashboard") ->
-    "Configuration for EMQX dashboard.";
-desc("listeners") ->
-    "Configuration for the dashboard listener.";
-desc("http") ->
-    "Configuration for the dashboard listener (plaintext).";
-desc("https") ->
-    "Configuration for the dashboard listener (TLS).";
-desc(_) ->
-    undefined.
+desc("dashboard") -> ?DESC(desc_dashboard);
+desc("listeners") -> ?DESC(desc_listeners);
+desc("http") -> ?DESC(desc_http);
+desc("https") -> ?DESC(desc_https);
+desc(_) -> undefined.
 
 bind(type) -> hoconsc:union([non_neg_integer(), emqx_schema:ip_port()]);
 bind(default) -> 18083;
 bind(required) -> true;
-bind(desc) -> "Port without IP(18083) or port with specified IP(127.0.0.1:18083).";
+bind(desc) -> ?DESC(bind);
 bind(_) -> undefined.
 
 default_username(type) -> string();
 default_username(default) -> "admin";
 default_username(required) -> true;
-default_username(desc) -> "The default username of the automatically created dashboard user.";
+default_username(desc) ->  ?DESC(default_username);
 default_username('readOnly') -> true;
 default_username(_) -> undefined.
 
@@ -180,11 +164,7 @@ default_password('readOnly') ->
 default_password(sensitive) ->
     true;
 default_password(desc) ->
-    ""
-    "\n"
-    "The initial default password for dashboard 'admin' user.\n"
-    "For safety, it should be changed as soon as possible."
-    "";
+    ?DESC(default_password);
 default_password(_) ->
     undefined.
 
@@ -195,18 +175,22 @@ cors(default) ->
 cors(required) ->
     false;
 cors(desc) ->
-    "Support Cross-Origin Resource Sharing (CORS).\n"
-    "Allows a server to indicate any origins (domain, scheme, or port) other than\n"
-    "its own from which a browser should permit loading resources.";
+    ?DESC(cors);
 cors(_) ->
     undefined.
 
 i18n_lang(type) -> ?ENUM([en, zh]);
 i18n_lang(default) -> en;
 i18n_lang('readOnly') -> true;
-i18n_lang(desc) -> "Internationalization language support.";
+i18n_lang(desc) -> ?DESC(i18n_lang);
 i18n_lang(_) -> undefined.
 
+validate_sample_interval(Second) ->
+    case Second >= 1 andalso Second =< 60 andalso (60 rem Second =:= 0) of
+        true -> ok;
+        false -> error({"Sample interval must be between 1 and 60 and be a divisor of 60.", Second})
+    end.
+
 sc(Type, Meta) -> hoconsc:mk(Type, Meta).
 
 ref(Field) -> hoconsc:ref(?MODULE, Field).

+ 93 - 0
apps/emqx_plugins/i18n/emqx_plugins_schema.conf

@@ -0,0 +1,93 @@
+emqx_plugins_schema {
+  plugins {
+    desc {
+      en: """
+Manage EMQX plugins.<br>
+Plugins can be pre-built as a part of EMQX package,
+or installed as a standalone package in a location specified by
+<code>install_dir</code> config key<br>
+The standalone-installed plugins are referred to as 'external' plugins.
+"""
+      zh: """管理EMQX插件。<br>
+插件可以是EMQX安装包中的一部分,也可以是一个独立的安装包。<br>
+独立安装的插件称为“外部插件”。
+           """
+    }
+    label {
+      en: "Plugins"
+      zh: "插件"
+    }
+  }
+  state {
+    desc {
+      en: "A per-plugin config to describe the desired state of the plugin."
+      zh: "描述插件的状态"
+    }
+    label {
+      en: "State"
+      zh: "插件状态"
+    }
+  }
+  name_vsn {
+    desc {
+      en: """The {name}-{version} of the plugin.<br>
+It should match the plugin application name-version as the for the plugin release package name<br>
+For example: my_plugin-0.1.0.
+"""
+      zh: """插件的名称{name}-{version}。<br>
+它应该与插件的发布包名称一致,如my_plugin-0.1.0。"""
+    }
+    label {
+      en: "Name-Version"
+      zh: "名称-版本"
+    }
+  }
+  enable {
+    desc {
+      en: "Set to 'true' to enable this plugin"
+      zh: "设置为“true”以启用此插件"
+    }
+    label {
+      en: "Enable"
+      zh: "启用"
+    }
+  }
+  states {
+    desc {
+      en: """An array of plugins in the desired states.<br>
+The plugins are started in the defined order"""
+      zh: """一组插件的状态。插件将按照定义的顺序启动"""
+    }
+    label {
+      en: "States"
+      zh: "插件启动顺序及状态"
+    }
+  }
+  install_dir {
+    desc {
+      en: """
+The installation directory for the external plugins.
+The plugin beam files and configuration files should reside in
+the subdirectory named as <code>emqx_foo_bar-0.1.0</code>.
+<br>
+NOTE: For security reasons, this directory should **NOT** be writable
+by anyone except <code>emqx</code> (or any user which runs EMQX).
+"""
+      zh: "插件安装包的目录, 不要自己创建, 只能由emqx用户创建与修改"
+    }
+    label {
+      en: "Install Directory"
+      zh: "安装目录"
+    }
+  }
+  check_interval {
+    desc {
+      en: """Check interval: check if the status of the plugins in the cluster is consistent, <br>
+if the results of 3 consecutive checks are not consistent, then alarm.
+"""
+      zh: """检查间隔:检查集群中插件的状态是否一致,<br>
+如果连续3次检查结果不一致,则报警。
+"""
+    }
+  }
+}

+ 8 - 30
apps/emqx_plugins/src/emqx_plugins_schema.erl

@@ -23,7 +23,7 @@
         , namespace/0
         ]).
 
--include_lib("typerefl/include/types.hrl").
+-include_lib("hocon/include/hoconsc.hrl").
 -include("emqx_plugins.hrl").
 
 namespace() -> "plugin".
@@ -32,33 +32,22 @@ roots() -> [?CONF_ROOT].
 
 fields(?CONF_ROOT) ->
     #{fields => root_fields(),
-      desc => """
-Manage EMQX plugins.
-<br>
-Plugins can be pre-built as a part of EMQX package,
-or installed as a standalone package in a location specified by
-<code>install_dir</code> config key
-<br>
-The standalone-installed plugins are referred to as 'external' plugins.
-"""
+      desc => ?DESC(?CONF_ROOT)
      };
 fields(state) ->
     #{ fields => state_fields(),
-       desc => "A per-plugin config to describe the desired state of the plugin."
+       desc => ?DESC(state)
      }.
 
 state_fields() ->
     [ {name_vsn,
        hoconsc:mk(string(),
-                  #{ desc => "The {name}-{version} of the plugin.<br>"
-                             "It should match the plugin application name-version as the "
-                             "for the plugin release package name<br>"
-                             "For example: my_plugin-0.1.0."
+                  #{ desc =>  ?DESC(name_vsn)
                    , required => true
                    })}
     , {enable,
        hoconsc:mk(boolean(),
-                  #{ desc => "Set to 'true' to enable this plugin"
+                  #{ desc => ?DESC(enable)
                    , required => true
                    })}
     ].
@@ -72,27 +61,16 @@ root_fields() ->
 states(type) -> hoconsc:array(hoconsc:ref(?MODULE, state));
 states(required) -> false;
 states(default) -> [];
-states(desc) -> "An array of plugins in the desired states.<br>"
-                "The plugins are started in the defined order";
+states(desc) -> ?DESC(states);
 states(_) -> undefined.
 
 install_dir(type) -> string();
 install_dir(required) -> false;
 install_dir(default) -> "plugins"; %% runner's root dir
 install_dir(T) when T =/= desc -> undefined;
-install_dir(desc) -> """
-The installation directory for the external plugins.
-The plugin beam files and configuration files should reside in
-the subdirectory named as <code>emqx_foo_bar-0.1.0</code>.
-<br>
-NOTE: For security reasons, this directory should **NOT** be writable
-by anyone except <code>emqx</code> (or any user which runs EMQX).
-""".
+install_dir(desc) -> ?DESC(install_dir).
 
 check_interval(type) -> emqx_schema:duration();
 check_interval(default) -> "5s";
 check_interval(T) when T =/= desc -> undefined;
-check_interval(desc) -> """
-Check interval: check if the status of the plugins in the cluster is consistent, <br>
-if the results of 3 consecutive checks are not consistent, then alarm.
-""".
+check_interval(desc) -> ?DESC(check_interval).

+ 1 - 1
scripts/merge-i18n.escript

@@ -3,7 +3,7 @@
 -mode(compile).
 
 main(_) ->
-    {ok, BaseConf} = file:read_file("apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf"),
+    {ok, BaseConf} = file:read_file("apps/emqx_dashboard/i18n/emqx_dashboard_schema.conf"),
 
     Cfgs = get_all_cfgs("apps/"),
     Conf = [merge(BaseConf, Cfgs),