Automatically detect language of posts, improve language detection when posting (#800)

* Use language detection to translate posts

The source language of a post is now determined via Apples internal language
detection, translation from the transmitted language is still possible.

* Make language detection posting more accessible

Language recognition is now always applied before posting, even if the user has
explicitly selected a different language. However, the user is always asked in
which of the two languages he wants to post.

* Add localizations

* Remove language detection in the timeline for now

The language detection in the timeline is for now removed to increase
timeline-performance.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

* Show translate button even if no language is sent

The translate-button is shown even if no language is sent with the post.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

* Adjust to new commits on main

Adjustments are made in regards to new developments on main.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

---------

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>
Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
Paul Schuetz 2023-02-12 18:23:29 +01:00 committed by GitHub
parent aab397f2bb
commit cd3c50e151
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 159 additions and 37 deletions

View file

@ -374,6 +374,8 @@
"status.editor.error.upload" = "Error en la pujada"; "status.editor.error.upload" = "Error en la pujada";
"status.editor.language-select.navigation-title" = "Selecciona la llengua"; "status.editor.language-select.navigation-title" = "Selecciona la llengua";
"status.editor.language-select.recently-used" = "Recents"; "status.editor.language-select.recently-used" = "Recents";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Edita la imatge"; "status.editor.media.edit-image" = "Edita la imatge";
"status.editor.media.image-description" = "Descripció de la imatge"; "status.editor.media.image-description" = "Descripció de la imatge";
"status.editor.mode.edit" = "Edita la publicació"; "status.editor.mode.edit" = "Edita la publicació";

View file

@ -375,6 +375,8 @@
"status.editor.error.upload" = "Fehler beim Hochladen"; "status.editor.error.upload" = "Fehler beim Hochladen";
"status.editor.language-select.navigation-title" = "Sprache auswählen"; "status.editor.language-select.navigation-title" = "Sprache auswählen";
"status.editor.language-select.recently-used" = "Kürzlich genutzt"; "status.editor.language-select.recently-used" = "Kürzlich genutzt";
"status.editor.language-select.confirmation.detected-%@" = "Auf %@ posten (Erkannte Sprache)";
"status.editor.language-select.confirmation.selected-%@" = "Auf %@ posten (Ausgewählte Sprache)";
"status.editor.media.edit-image" = "Bild bearbeiten"; "status.editor.media.edit-image" = "Bild bearbeiten";
"status.editor.media.image-description" = "Bildbeschreibung"; "status.editor.media.image-description" = "Bildbeschreibung";
"status.editor.mode.edit" = "Deinen Beitrag bearbeiten"; "status.editor.mode.edit" = "Deinen Beitrag bearbeiten";

View file

@ -375,6 +375,8 @@
"status.editor.error.upload" = "Error uploading"; "status.editor.error.upload" = "Error uploading";
"status.editor.language-select.navigation-title" = "Select Language"; "status.editor.language-select.navigation-title" = "Select Language";
"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.language-select.recently-used" = "Recently Used";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Edit Image"; "status.editor.media.edit-image" = "Edit Image";
"status.editor.media.image-description" = "Image description"; "status.editor.media.image-description" = "Image description";
"status.editor.mode.edit" = "Editing your post"; "status.editor.mode.edit" = "Editing your post";

View file

@ -376,6 +376,8 @@
"status.editor.error.upload" = "Error uploading"; "status.editor.error.upload" = "Error uploading";
"status.editor.language-select.navigation-title" = "Select Language"; "status.editor.language-select.navigation-title" = "Select Language";
"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.language-select.recently-used" = "Recently Used";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Edit Image"; "status.editor.media.edit-image" = "Edit Image";
"status.editor.media.image-description" = "Image description"; "status.editor.media.image-description" = "Image description";
"status.editor.mode.edit" = "Editing your post"; "status.editor.mode.edit" = "Editing your post";

View file

@ -376,6 +376,8 @@
"status.editor.error.upload" = "Error subiendo"; "status.editor.error.upload" = "Error subiendo";
"status.editor.language-select.navigation-title" = "Seleccionar idioma"; "status.editor.language-select.navigation-title" = "Seleccionar idioma";
"status.editor.language-select.recently-used" = "Usado recientemente"; "status.editor.language-select.recently-used" = "Usado recientemente";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Editar Imagen"; "status.editor.media.edit-image" = "Editar Imagen";
"status.editor.media.image-description" = "Descripción de la imagen"; "status.editor.media.image-description" = "Descripción de la imagen";
"status.editor.mode.edit" = "Editando tu publicación"; "status.editor.mode.edit" = "Editando tu publicación";

View file

@ -375,6 +375,8 @@
"status.editor.error.upload" = "Errorea igotzerakoan"; "status.editor.error.upload" = "Errorea igotzerakoan";
"status.editor.language-select.navigation-title" = "Hautatu hizkuntza"; "status.editor.language-select.navigation-title" = "Hautatu hizkuntza";
"status.editor.language-select.recently-used" = "Duela gutxi erabilitakoak"; "status.editor.language-select.recently-used" = "Duela gutxi erabilitakoak";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Editatu irudiak"; "status.editor.media.edit-image" = "Editatu irudiak";
"status.editor.media.image-description" = "Irudiaren deskribapena"; "status.editor.media.image-description" = "Irudiaren deskribapena";
"status.editor.mode.edit" = "Bidalketa editatzen"; "status.editor.mode.edit" = "Bidalketa editatzen";

View file

@ -371,6 +371,8 @@
"status.editor.error.upload" = "Erreur de téléchargement"; "status.editor.error.upload" = "Erreur de téléchargement";
"status.editor.language-select.navigation-title" = "Sélectionner la langue"; "status.editor.language-select.navigation-title" = "Sélectionner la langue";
"status.editor.language-select.recently-used" = "Utilisé récemment"; "status.editor.language-select.recently-used" = "Utilisé récemment";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Modifier l'image"; "status.editor.media.edit-image" = "Modifier l'image";
"status.editor.media.image-description" = "Description de l'image"; "status.editor.media.image-description" = "Description de l'image";
"status.editor.mode.edit" = "Modification de votre publication"; "status.editor.mode.edit" = "Modification de votre publication";

View file

@ -376,6 +376,8 @@
"status.editor.error.upload" = "Errore durante il caricamento"; "status.editor.error.upload" = "Errore durante il caricamento";
"status.editor.language-select.navigation-title" = "Scegli la lingua"; "status.editor.language-select.navigation-title" = "Scegli la lingua";
"status.editor.language-select.recently-used" = "Usato recentemente"; "status.editor.language-select.recently-used" = "Usato recentemente";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Modifica l'immagine"; "status.editor.media.edit-image" = "Modifica l'immagine";
"status.editor.media.image-description" = "Descrizione dell'immagine"; "status.editor.media.image-description" = "Descrizione dell'immagine";
"status.editor.mode.edit" = "Modifica post"; "status.editor.mode.edit" = "Modifica post";

View file

@ -375,6 +375,8 @@
"status.editor.error.upload" = "アップロードエラー"; "status.editor.error.upload" = "アップロードエラー";
"status.editor.language-select.navigation-title" = "言語設定"; "status.editor.language-select.navigation-title" = "言語設定";
"status.editor.language-select.recently-used" = "最近使用した"; "status.editor.language-select.recently-used" = "最近使用した";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "イメージの編集"; "status.editor.media.edit-image" = "イメージの編集";
"status.editor.media.image-description" = "イメージの説明文"; "status.editor.media.image-description" = "イメージの説明文";
"status.editor.mode.edit" = "投稿を編集する"; "status.editor.mode.edit" = "投稿を編集する";

View file

@ -377,6 +377,8 @@
"status.editor.error.upload" = "전송 오류"; "status.editor.error.upload" = "전송 오류";
"status.editor.language-select.navigation-title" = "언어 선택"; "status.editor.language-select.navigation-title" = "언어 선택";
"status.editor.language-select.recently-used" = "최근 사용"; "status.editor.language-select.recently-used" = "최근 사용";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "이미지 편집"; "status.editor.media.edit-image" = "이미지 편집";
"status.editor.media.image-description" = "이미지 설명"; "status.editor.media.image-description" = "이미지 설명";
"status.editor.mode.edit" = "글 수정"; "status.editor.mode.edit" = "글 수정";

View file

@ -375,6 +375,8 @@
"status.editor.error.upload" = "Feil ved opplasting"; "status.editor.error.upload" = "Feil ved opplasting";
"status.editor.language-select.navigation-title" = "Velg språk"; "status.editor.language-select.navigation-title" = "Velg språk";
"status.editor.language-select.recently-used" = "Nylig brukt"; "status.editor.language-select.recently-used" = "Nylig brukt";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Rediger bilde"; "status.editor.media.edit-image" = "Rediger bilde";
"status.editor.media.image-description" = "Bildebeskrivelse"; "status.editor.media.image-description" = "Bildebeskrivelse";
"status.editor.mode.edit" = "Redigerer innlegget ditt"; "status.editor.mode.edit" = "Redigerer innlegget ditt";

View file

@ -369,6 +369,8 @@
"status.editor.error.upload" = "Fout tijdens uploaden"; "status.editor.error.upload" = "Fout tijdens uploaden";
"status.editor.language-select.navigation-title" = "Taal selecteren"; "status.editor.language-select.navigation-title" = "Taal selecteren";
"status.editor.language-select.recently-used" = "Onlangs gebruikt"; "status.editor.language-select.recently-used" = "Onlangs gebruikt";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Afbeelding bewerken"; "status.editor.media.edit-image" = "Afbeelding bewerken";
"status.editor.media.image-description" = "Omschrijving"; "status.editor.media.image-description" = "Omschrijving";
"status.editor.mode.edit" = "Post bewerken"; "status.editor.mode.edit" = "Post bewerken";

View file

@ -371,6 +371,8 @@
"status.editor.error.upload" = "Błąd wysyłania"; "status.editor.error.upload" = "Błąd wysyłania";
"status.editor.language-select.navigation-title" = "Wybierz język"; "status.editor.language-select.navigation-title" = "Wybierz język";
"status.editor.language-select.recently-used" = "Ostatnio użyty"; "status.editor.language-select.recently-used" = "Ostatnio użyty";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Edytuj obrazek"; "status.editor.media.edit-image" = "Edytuj obrazek";
"status.editor.media.image-description" = "Opis obrazka"; "status.editor.media.image-description" = "Opis obrazka";
"status.editor.mode.edit" = "Edycja twojego postu"; "status.editor.mode.edit" = "Edycja twojego postu";

View file

@ -375,6 +375,8 @@
"status.editor.error.upload" = "Erro ao fazer upload"; "status.editor.error.upload" = "Erro ao fazer upload";
"status.editor.language-select.navigation-title" = "Selecionar Idioma"; "status.editor.language-select.navigation-title" = "Selecionar Idioma";
"status.editor.language-select.recently-used" = "Usado recentemente"; "status.editor.language-select.recently-used" = "Usado recentemente";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Editar Imagem"; "status.editor.media.edit-image" = "Editar Imagem";
"status.editor.media.image-description" = "Descrição da imagem"; "status.editor.media.image-description" = "Descrição da imagem";
"status.editor.mode.edit" = "Editando sua postagem"; "status.editor.mode.edit" = "Editando sua postagem";

View file

@ -371,6 +371,8 @@
"status.editor.error.upload" = "Yüklerken Hata Oluştu"; "status.editor.error.upload" = "Yüklerken Hata Oluştu";
"status.editor.language-select.navigation-title" = "Dil Seç"; "status.editor.language-select.navigation-title" = "Dil Seç";
"status.editor.language-select.recently-used" = "Son Kullanılanlar"; "status.editor.language-select.recently-used" = "Son Kullanılanlar";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "Görüntüyü Düzenle"; "status.editor.media.edit-image" = "Görüntüyü Düzenle";
"status.editor.media.image-description" = "Görüntü Açıklaması"; "status.editor.media.image-description" = "Görüntü Açıklaması";
"status.editor.mode.edit" = "Gönderin Düzenleniyor"; "status.editor.mode.edit" = "Gönderin Düzenleniyor";

View file

@ -376,6 +376,8 @@
"status.editor.error.upload" = "上传错误"; "status.editor.error.upload" = "上传错误";
"status.editor.language-select.navigation-title" = "选择语言"; "status.editor.language-select.navigation-title" = "选择语言";
"status.editor.language-select.recently-used" = "最近使用"; "status.editor.language-select.recently-used" = "最近使用";
"status.editor.language-select.confirmation.detected-%@" = "Post in %@ (Detected language)";
"status.editor.language-select.confirmation.selected-%@" = "Post in %@ (Selected language)";
"status.editor.media.edit-image" = "编辑图片"; "status.editor.media.edit-image" = "编辑图片";
"status.editor.media.image-description" = "图片描述"; "status.editor.media.image-description" = "图片描述";
"status.editor.mode.edit" = "正在编辑你的嘟文"; "status.editor.mode.edit" = "正在编辑你的嘟文";

View file

@ -21,6 +21,7 @@ public struct StatusEditorView: View {
@FocusState private var isSpoilerTextFocused: Bool @FocusState private var isSpoilerTextFocused: Bool
@State private var isDismissAlertPresented: Bool = false @State private var isDismissAlertPresented: Bool = false
@State private var isLanguageConfirmPresented = false
public init(mode: StatusEditorViewModel.Mode) { public init(mode: StatusEditorViewModel.Mode) {
_viewModel = StateObject(wrappedValue: .init(mode: mode)) _viewModel = StateObject(wrappedValue: .init(mode: mode))
@ -104,12 +105,12 @@ public struct StatusEditorView: View {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
Task { Task {
let status = await viewModel.postStatus() viewModel.evaluateLanguages()
if status != nil { if let _ = viewModel.languageConfirmationDialogLanguages {
dismiss() isLanguageConfirmPresented = true
NotificationCenter.default.post(name: NotificationsName.shareSheetClose, } else {
object: nil) await postStatus()
} }
} }
} label: { } label: {
if viewModel.isPosting { if viewModel.isPosting {
@ -120,6 +121,9 @@ public struct StatusEditorView: View {
} }
.disabled(!viewModel.canPost) .disabled(!viewModel.canPost)
.keyboardShortcut(.return, modifiers: .command) .keyboardShortcut(.return, modifiers: .command)
.confirmationDialog("", isPresented: $isLanguageConfirmPresented, actions: {
languageConfirmationDialog
})
} }
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button { Button {
@ -156,6 +160,42 @@ public struct StatusEditorView: View {
.interactiveDismissDisabled(!viewModel.statusText.string.isEmpty) .interactiveDismissDisabled(!viewModel.statusText.string.isEmpty)
} }
@ViewBuilder
private var languageConfirmationDialog: some View {
if let dialogVals = viewModel.languageConfirmationDialogLanguages,
let detected = dialogVals["detected"],
let detectedLong = Locale.current.localizedString(forLanguageCode: detected),
let selected = dialogVals["selected"],
let selectedLong = Locale.current.localizedString(forLanguageCode: selected){
Button("status.editor.language-select.confirmation.detected-\(detectedLong)") {
viewModel.selectedLanguage = detected
Task {
await postStatus()
}
}
Button("status.editor.language-select.confirmation.selected-\(selectedLong)") {
viewModel.selectedLanguage = selected
Task {
await postStatus()
}
}
Button("action.cancel", role: .cancel) {
viewModel.languageConfirmationDialogLanguages = nil
}
} else {
EmptyView()
}
}
private func postStatus() async {
let status = await viewModel.postStatus()
if status != nil {
dismiss()
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil)
}
}
@ViewBuilder @ViewBuilder
private var spoilerTextView: some View { private var spoilerTextView: some View {
if viewModel.spoilerOn { if viewModel.spoilerOn {

View file

@ -14,6 +14,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
var currentAccount: Account? var currentAccount: Account?
var theme: Theme? var theme: Theme?
var preferences: UserPreferences? var preferences: UserPreferences?
var languageConfirmationDialogLanguages: [String: String]?
var textView: UITextView? { var textView: UITextView? {
didSet { didSet {
@ -141,6 +142,17 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
selectedLanguage = selectedLanguage ?? preference ?? currentAccount?.source?.language selectedLanguage = selectedLanguage ?? preference ?? currentAccount?.source?.language
} }
func evaluateLanguages(){
if let detectedLang = detectLanguage(text: statusText.string),
let selectedLanguage = selectedLanguage,
selectedLanguage != detectedLang {
languageConfirmationDialogLanguages = ["detected": detectedLang,
"selected": selectedLanguage]
} else {
languageConfirmationDialogLanguages = nil;
}
}
func postStatus() async -> Status? { func postStatus() async -> Status? {
guard let client else { return nil } guard let client else { return nil }
do { do {
@ -153,20 +165,6 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
expires_in: pollDuration.rawValue) expires_in: pollDuration.rawValue)
} }
if !hasExplicitlySelectedLanguage {
// Attempt language resolution using Natural Language
let recognizer = NLLanguageRecognizer()
recognizer.processString(statusText.string)
// Use languageHypotheses to get the probability with it
let hypotheses = recognizer.languageHypotheses(withMaximum: 1)
// Assert that 85% probability is enough :)
// A one word toot that is en/fr compatible is only ~50% confident, for instance
if let (language, probability) = hypotheses.first, probability > 0.85 {
// rawValue return the IETF BCP 47 language tag
selectedLanguage = language.rawValue
}
}
let data = StatusData(status: statusText.string, let data = StatusData(status: statusText.string,
visibility: visibility, visibility: visibility,
inReplyToId: mode.replyToStatus?.id, inReplyToId: mode.replyToStatus?.id,

View file

@ -0,0 +1,34 @@
import Foundation
import NaturalLanguage
private func stripToPureLanguage(inText: String) -> String {
let hashtagRegex = try! Regex("#[\\w]*")
let emojiRegex = try! Regex(":\\w*:")
let atRegex = try! Regex("@\\w*")
var resultStr = inText
[hashtagRegex, emojiRegex, atRegex].forEach { regex in
let splitArray = resultStr.split(separator: regex, omittingEmptySubsequences: true)
resultStr = splitArray.joined() as String
}
return resultStr.trimmingCharacters(in: .whitespacesAndNewlines)
}
func detectLanguage(text: String) -> String? {
let recognizer = NLLanguageRecognizer()
let strippedText = stripToPureLanguage(inText: text)
recognizer.processString(strippedText)
let hypotheses = recognizer.languageHypotheses(withMaximum: 1)
// Use the detected language only with >= 85 % confidence
if let (lang, confidence) = hypotheses.first, confidence >= 0.85 {
return lang.rawValue
} else {
return nil
}
}

View file

@ -82,9 +82,8 @@ struct StatusRowContextMenu: View {
await viewModel.translate(userLang: lang) await viewModel.translate(userLang: lang)
} }
} label: { } label: {
if let statusLang = viewModel.status.language, if let statusLang = viewModel.getStatusLang(),
let languageName = Locale.current.localizedString(forLanguageCode: statusLang) let languageName = Locale.current.localizedString(forLanguageCode: statusLang) {
{
Label("status.action.translate-from-\(languageName)", systemImage: "captions.bubble") Label("status.action.translate-from-\(languageName)", systemImage: "captions.bubble")
} else { } else {
Label("status.action.translate", systemImage: "captions.bubble") Label("status.action.translate", systemImage: "captions.bubble")

View file

@ -345,14 +345,24 @@ public struct StatusRowView: View {
.accessibilityHidden(true) .accessibilityHidden(true)
} }
private func shouldShowTranslateButton(status: AnyStatus) -> Bool {
let statusLang = viewModel.getStatusLang()
if let userLang = preferences.serverPreferences?.postLanguage,
preferences.showTranslateButton,
!status.content.asRawText.isEmpty,
viewModel.translation == nil
{
return userLang != statusLang
} else {
return false
}
}
@ViewBuilder @ViewBuilder
private func makeTranslateView(status: AnyStatus) -> some View { private func makeTranslateView(status: AnyStatus) -> some View {
if let userLang = preferences.serverPreferences?.postLanguage, if let userLang = preferences.serverPreferences?.postLanguage,
preferences.showTranslateButton, shouldShowTranslateButton(status: status)
status.language != nil,
userLang != status.language,
!status.content.asRawText.isEmpty,
viewModel.translation == nil
{ {
Button { Button {
Task { Task {
@ -362,13 +372,13 @@ public struct StatusRowView: View {
if viewModel.isLoadingTranslation { if viewModel.isLoadingTranslation {
ProgressView() ProgressView()
} else { } else {
if let statusLanguage = status.language, if let statusLanguage = viewModel.getStatusLang(),
let languageName = Locale.current.localizedString(forLanguageCode: statusLanguage) let languageName = Locale.current.localizedString(forLanguageCode: statusLanguage)
{ {
Text("status.action.translate-from-\(languageName)") Text("status.action.translate-from-\(languageName)")
} else { } else {
Text("status.action.translate") Text("status.action.translate")
} }
} }
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)

View file

@ -2,6 +2,7 @@ import Env
import Models import Models
import Network import Network
import SwiftUI import SwiftUI
import NaturalLanguage
import DesignSystem import DesignSystem
@ -289,8 +290,16 @@ public class StatusRowViewModel: ObservableObject {
reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount
repliesCount = status.reblog?.repliesCount ?? status.repliesCount repliesCount = status.reblog?.repliesCount ?? status.repliesCount
} }
func getStatusLang() -> String? {
status.language
}
func translate(userLang: String) async { func translate(userLang: String) async {
await translate(userLang: userLang, sourceLang: getStatusLang())
}
private func translate(userLang: String, sourceLang: String?) async {
guard let client else { return } guard let client else { return }
do { do {
withAnimation { withAnimation {