]> Untitled Git - lemmy.git/commitdiff
WIP: Email localization (fixes #500) (#2053)
authorNutomic <me@nutomic.com>
Thu, 24 Mar 2022 15:25:51 +0000 (15:25 +0000)
committerGitHub <noreply@github.com>
Thu, 24 Mar 2022 15:25:51 +0000 (15:25 +0000)
* Allow email localization (fixes #500)

* add PersonAggregates::default()

* add lemmy-translations submodule

* fix gitmodules

14 files changed:
.drone.yml
.gitmodules [new file with mode: 0644]
Cargo.lock
crates/api/src/local_user.rs
crates/api_common/Cargo.toml
crates/api_common/src/lib.rs
crates/api_crud/src/private_message/create.rs
crates/api_crud/src/user/create.rs
crates/db_schema/src/aggregates/person_aggregates.rs
crates/utils/Cargo.toml
crates/utils/build.rs [new file with mode: 0644]
crates/utils/src/email.rs
crates/utils/translations [new submodule]
crates/websocket/src/send.rs

index 001714545f27dd3a2662f55af73855b513758481..311a116ccbe556188a7b9d5920ecab3da9a572eb 100644 (file)
@@ -14,6 +14,8 @@ steps:
     commands:
       - chown 1000:1000 . -R
       - git fetch --tags
+      - git submodule init
+      - git submodule update --recursive --remote
 
   - name: check formatting
     image: rustdocker/rust:nightly
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..f673c7a
--- /dev/null
@@ -0,0 +1,4 @@
+[submodule "crates/utils/translations"]
+       path = crates/utils/translations
+       url = https://github.com/LemmyNet/lemmy-translations.git
+        branch = main
index ad273ba184153618860335c0ce2eaf6189132179..1c9e75c3cee1f0c252949c4eedf03ba023b04fee 100644 (file)
@@ -1884,6 +1884,7 @@ dependencies = [
  "lemmy_db_views_actor",
  "lemmy_db_views_moderator",
  "lemmy_utils",
+ "rosetta-i18n",
  "serde",
  "serde_json",
  "tracing",
@@ -2170,6 +2171,8 @@ dependencies = [
  "regex",
  "reqwest",
  "reqwest-middleware",
+ "rosetta-build",
+ "rosetta-i18n",
  "serde",
  "serde_json",
  "smart-default",
@@ -3368,6 +3371,26 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "rosetta-build"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f697b8b3f19bee20f30dc87213d05ce091c43bc733ab1bfc98b0e5cdd9943f3"
+dependencies = [
+ "convert_case",
+ "lazy_static",
+ "proc-macro2 1.0.33",
+ "quote 1.0.10",
+ "regex",
+ "tinyjson",
+]
+
+[[package]]
+name = "rosetta-i18n"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5298de832602aecc9458398f435d9bff0be57da7aac11221b6ff3d4ef9503de"
+
 [[package]]
 name = "rss"
 version = "2.0.0"
@@ -3902,6 +3925,12 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6"
 
+[[package]]
+name = "tinyjson"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a8304da9f9370f6a6f9020b7903b044aa9ce3470f300a1fba5bc77c78145a16"
+
 [[package]]
 name = "tinyvec"
 version = "1.5.1"
index 0b2a4c3aa1bf4c05426e51dda7e7702d28bb30b6..0819c98f26f33889d5215592e5ce41aa6efc16b1 100644 (file)
@@ -189,17 +189,11 @@ impl Perform for SaveUserSettings {
     let email = diesel_option_overwrite(&email_deref);
 
     if let Some(Some(email)) = &email {
-      let previous_email = local_user_view.local_user.email.unwrap_or_default();
+      let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
       // Only send the verification email if there was an email change
       if previous_email.ne(email) {
-        send_verification_email(
-          local_user_view.local_user.id,
-          email,
-          &local_user_view.person.name,
-          context.pool(),
-          &context.settings(),
-        )
-        .await?;
+        send_verification_email(&local_user_view, email, context.pool(), &context.settings())
+          .await?;
       }
     }
 
index 0b41519131bc887a4bdbb6bd1ba15ab944f2a415..7d239a64dabd91ecdb61381f5c7c427b49db58af 100644 (file)
@@ -26,3 +26,4 @@ serde_json = { version = "1.0.72", features = ["preserve_order"] }
 tracing = "0.1.29"
 url = "2.2.2"
 itertools = "0.10.3"
+rosetta-i18n = "0.1"
index 68ad36744856295828a32d71c006c0910b6c984f..50919b3cb64b465c6a21c5ecf4f9ae0717a3afc7 100644 (file)
@@ -33,12 +33,14 @@ use lemmy_db_views_actor::{
 };
 use lemmy_utils::{
   claims::Claims,
-  email::send_email,
+  email::{send_email, translations::Lang},
   settings::structs::{FederationConfig, Settings},
   utils::generate_random_string,
   LemmyError,
   Sensitive,
 };
+use rosetta_i18n::{Language, LanguageId};
+use tracing::warn;
 use url::Url;
 
 pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
@@ -363,9 +365,8 @@ pub fn honeypot_check(honeypot: &Option<String>) -> Result<(), LemmyError> {
 
 pub fn send_email_to_user(
   local_user_view: &LocalUserView,
-  subject_text: &str,
-  body_text: &str,
-  comment_content: &str,
+  subject: &str,
+  body: &str,
   settings: &Settings,
 ) {
   if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
@@ -373,32 +374,21 @@ pub fn send_email_to_user(
   }
 
   if let Some(user_email) = &local_user_view.local_user.email {
-    let subject = &format!(
-      "{} - {} {}",
-      subject_text, settings.hostname, local_user_view.person.name,
-    );
-    let html = &format!(
-      "<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
-      body_text,
-      local_user_view.person.name,
-      comment_content,
-      settings.get_protocol_and_hostname()
-    );
     match send_email(
       subject,
       user_email,
       &local_user_view.person.name,
-      html,
+      body,
       settings,
     ) {
       Ok(_o) => _o,
-      Err(e) => tracing::error!("{}", e),
+      Err(e) => warn!("{}", e),
     };
   }
 }
 
 pub async fn send_password_reset_email(
-  local_user_view: &LocalUserView,
+  user: &LocalUserView,
   pool: &DbPool,
   settings: &Settings,
 ) -> Result<(), LemmyError> {
@@ -407,29 +397,30 @@ pub async fn send_password_reset_email(
 
   // Insert the row
   let token2 = token.clone();
-  let local_user_id = local_user_view.local_user.id;
+  let local_user_id = user.local_user.id;
   blocking(pool, move |conn| {
     PasswordResetRequest::create_token(conn, local_user_id, &token2)
   })
   .await??;
 
-  let email = &local_user_view.local_user.email.to_owned().expect("email");
-  let subject = &format!("Password reset for {}", local_user_view.person.name);
+  let email = &user.local_user.email.to_owned().expect("email");
+  let lang = get_user_lang(user);
+  let subject = &lang.password_reset_subject(&user.person.name);
   let protocol_and_hostname = settings.get_protocol_and_hostname();
-  let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", local_user_view.person.name, protocol_and_hostname, &token);
-  send_email(subject, email, &local_user_view.person.name, html, settings)
+  let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token);
+  let body = &lang.password_reset_body(&user.person.name, reset_link);
+  send_email(subject, email, &user.person.name, body, settings)
 }
 
 /// Send a verification email
 pub async fn send_verification_email(
-  local_user_id: LocalUserId,
+  user: &LocalUserView,
   new_email: &str,
-  username: &str,
   pool: &DbPool,
   settings: &Settings,
 ) -> Result<(), LemmyError> {
   let form = EmailVerificationForm {
-    local_user_id,
+    local_user_id: user.local_user.id,
     email: new_email.to_string(),
     verification_token: generate_random_string(),
   };
@@ -440,44 +431,42 @@ pub async fn send_verification_email(
   );
   blocking(pool, move |conn| EmailVerification::create(conn, &form)).await??;
 
-  let subject = format!("Verify your email address for {}", settings.hostname);
-  let body = format!(
-    concat!(
-      "Please click the link below to verify your email address ",
-      "for the account @{}@{}. Ignore this email if the account isn't yours.<br><br>",
-      "<a href=\"{}\">Verify your email</a>"
-    ),
-    username, settings.hostname, verify_link
-  );
-  send_email(&subject, new_email, username, &body, settings)?;
+  let lang = get_user_lang(user);
+  let subject = lang.verify_email_subject(&settings.hostname);
+  let body = lang.verify_email_body(&user.person.name, &settings.hostname, verify_link);
+  send_email(&subject, new_email, &user.person.name, &body, settings)?;
 
   Ok(())
 }
 
 pub fn send_email_verification_success(
-  local_user_view: &LocalUserView,
+  user: &LocalUserView,
   settings: &Settings,
 ) -> Result<(), LemmyError> {
-  let email = &local_user_view.local_user.email.to_owned().expect("email");
-  let subject = &format!("Email verified for {}", local_user_view.person.actor_id);
-  let html = "Your email has been verified.";
-  send_email(subject, email, &local_user_view.person.name, html, settings)
+  let email = &user.local_user.email.to_owned().expect("email");
+  let lang = get_user_lang(user);
+  let subject = &lang.email_verified_subject(&user.person.actor_id);
+  let body = &lang.email_verified_body();
+  send_email(subject, email, &user.person.name, body, settings)
+}
+
+pub fn get_user_lang(user: &LocalUserView) -> Lang {
+  let user_lang = LanguageId::new(user.local_user.lang.clone());
+  Lang::from_language_id(&user_lang).unwrap_or_else(|| {
+    let en = LanguageId::new("en");
+    Lang::from_language_id(&en).expect("default language")
+  })
 }
 
 pub fn send_application_approved_email(
-  local_user_view: &LocalUserView,
+  user: &LocalUserView,
   settings: &Settings,
 ) -> Result<(), LemmyError> {
-  let email = &local_user_view.local_user.email.to_owned().expect("email");
-  let subject = &format!(
-    "Registration approved for {}",
-    local_user_view.person.actor_id
-  );
-  let html = &format!(
-    "Your registration application has been approved. Welcome to {}!",
-    settings.hostname
-  );
-  send_email(subject, email, &local_user_view.person.name, html, settings)
+  let email = &user.local_user.email.to_owned().expect("email");
+  let lang = get_user_lang(user);
+  let subject = lang.registration_approved_subject(&user.person.actor_id);
+  let body = lang.registration_approved_body(&settings.hostname);
+  send_email(&subject, email, &user.person.name, &body, settings)
 }
 
 pub async fn check_registration_application(
index ad7fd4adf9e4eed768e1dd99a4614322d2c9532a..44999cf0058d40db934ed72543a9414452c0e951 100644 (file)
@@ -4,6 +4,7 @@ use lemmy_api_common::{
   blocking,
   check_person_block,
   get_local_user_view_from_jwt,
+  get_user_lang,
   person::{CreatePrivateMessage, PrivateMessageResponse},
   send_email_to_user,
 };
@@ -106,11 +107,16 @@ impl PerformCrud for CreatePrivateMessage {
         LocalUserView::read_person(conn, recipient_id)
       })
       .await??;
+      let lang = get_user_lang(&local_recipient);
+      let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
       send_email_to_user(
         &local_recipient,
-        "Private Message from",
-        "Private Message",
-        &content_slurs_removed,
+        &lang.notification_mentioned_by_subject(&local_recipient.person.name),
+        &lang.notification_mentioned_by_body(
+          &local_recipient.person.name,
+          &content_slurs_removed,
+          &inbox_link,
+        ),
         &context.settings(),
       );
     }
index be746d2ae39b34346794f8e3427c00582661a34f..00ef7db64fe12431beac3006285f119f228e0712 100644 (file)
@@ -15,6 +15,7 @@ use lemmy_apub::{
   EndpointType,
 };
 use lemmy_db_schema::{
+  aggregates::person_aggregates::PersonAggregates,
   newtypes::CommunityId,
   source::{
     community::{
@@ -32,6 +33,7 @@ use lemmy_db_schema::{
   },
   traits::{Crud, Followable, Joinable},
 };
+use lemmy_db_views::local_user_view::LocalUserView;
 use lemmy_db_views_actor::person_view::PersonViewSafe;
 use lemmy_utils::{
   apub::generate_actor_keypair,
@@ -272,11 +274,20 @@ impl PerformCrud for Register {
       );
     } else {
       if email_verification {
+        let local_user_view = LocalUserView {
+          local_user: inserted_local_user,
+          person: inserted_person,
+          counts: PersonAggregates::default(),
+        };
+        // we check at the beginning of this method that email is set
+        let email = local_user_view
+          .local_user
+          .email
+          .clone()
+          .expect("email was provided");
         send_verification_email(
-          inserted_local_user.id,
-          // we check at the beginning of this method that email is set
-          &inserted_local_user.email.expect("email was provided"),
-          &inserted_person.name,
+          &local_user_view,
+          &email,
           context.pool(),
           &context.settings(),
         )
index 344ec27d9aed81c0668fd5e123241266ebe51027..e0fc0734c39467ac58e29556a95bd1e192d8298f 100644 (file)
@@ -3,7 +3,7 @@ use diesel::{result::Error, *};
 use serde::{Deserialize, Serialize};
 
 #[derive(
-  Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone,
+  Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone, Default,
 )]
 #[table_name = "person_aggregates"]
 pub struct PersonAggregates {
index 7391bcbadb040f708b22748b12a2a9e3cfaa334b..3aa96d970d7b4f2adb3af4945a5ad009ad55f614 100644 (file)
@@ -47,3 +47,7 @@ doku = "0.10.2"
 uuid = { version = "0.8.2", features = ["serde", "v4"] }
 encoding = "0.2.33"
 html2text = "0.2.1"
+rosetta-i18n = "0.1"
+
+[build-dependencies]
+rosetta-build = "0.1"
diff --git a/crates/utils/build.rs b/crates/utils/build.rs
new file mode 100644 (file)
index 0000000..8fcef5c
--- /dev/null
@@ -0,0 +1,8 @@
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+  rosetta_build::config()
+    .source("en", "translations/email/en.json")
+    .fallback("en")
+    .generate()?;
+
+  Ok(())
+}
index dfd66436b6c218adfcc82333fd20ee96408ab478..b1d58c7ef7672bebcc5beb68a74c76436d8a43b1 100644 (file)
@@ -11,6 +11,10 @@ use lettre::{
 use std::str::FromStr;
 use uuid::Uuid;
 
+pub mod translations {
+  rosetta_i18n::include_translations!();
+}
+
 pub fn send_email(
   subject: &str,
   to_email: &str,
diff --git a/crates/utils/translations b/crates/utils/translations
new file mode 160000 (submodule)
index 0000000..1314f10
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 1314f10fbc0db9c16ff4209a2885431024a14ed8
index 36e93fb695d4fe9d4f4f0d36a2a4beee690f0e08..1f0677d7d666a5cd550826bdb65a378d88889077 100644 (file)
@@ -8,6 +8,7 @@ use lemmy_api_common::{
   check_person_block,
   comment::CommentResponse,
   community::CommunityResponse,
+  get_user_lang,
   person::PrivateMessageResponse,
   post::PostResponse,
   send_email_to_user,
@@ -183,6 +184,7 @@ pub async fn send_local_notifs(
   context: &LemmyContext,
 ) -> Result<Vec<LocalUserId>, LemmyError> {
   let mut recipient_ids = Vec::new();
+  let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
 
   // Send the local mentions
   for mention in mentions
@@ -217,11 +219,11 @@ pub async fn send_local_notifs(
 
       // Send an email to those local users that have notifications on
       if do_send_email {
+        let lang = get_user_lang(&mention_user_view);
         send_email_to_user(
           &mention_user_view,
-          "Mentioned by",
-          "Person Mention",
-          &comment.content,
+          &lang.notification_mentioned_by_subject(&person.name),
+          &lang.notification_mentioned_by_body(&person.name, &comment.content, &inbox_link),
           &context.settings(),
         )
       }
@@ -252,11 +254,11 @@ pub async fn send_local_notifs(
             recipient_ids.push(parent_user_view.local_user.id);
 
             if do_send_email {
+              let lang = get_user_lang(&parent_user_view);
               send_email_to_user(
                 &parent_user_view,
-                "Reply from",
-                "Comment Reply",
-                &comment.content,
+                &lang.notification_post_reply_subject(&person.name),
+                &lang.notification_post_reply_body(&person.name, &comment.content, &inbox_link),
                 &context.settings(),
               )
             }
@@ -282,11 +284,11 @@ pub async fn send_local_notifs(
           recipient_ids.push(parent_user_view.local_user.id);
 
           if do_send_email {
+            let lang = get_user_lang(&parent_user_view);
             send_email_to_user(
               &parent_user_view,
-              "Reply from",
-              "Post Reply",
-              &comment.content,
+              &lang.notification_post_reply_subject(&person.name),
+              &lang.notification_post_reply_body(&person.name, &comment.content, &inbox_link),
               &context.settings(),
             )
           }