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:
parent
aab397f2bb
commit
cd3c50e151
|
@ -374,6 +374,8 @@
|
|||
"status.editor.error.upload" = "Error en la pujada";
|
||||
"status.editor.language-select.navigation-title" = "Selecciona la llengua";
|
||||
"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.image-description" = "Descripció de la imatge";
|
||||
"status.editor.mode.edit" = "Edita la publicació";
|
||||
|
|
|
@ -375,6 +375,8 @@
|
|||
"status.editor.error.upload" = "Fehler beim Hochladen";
|
||||
"status.editor.language-select.navigation-title" = "Sprache auswählen";
|
||||
"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.image-description" = "Bildbeschreibung";
|
||||
"status.editor.mode.edit" = "Deinen Beitrag bearbeiten";
|
||||
|
|
|
@ -375,6 +375,8 @@
|
|||
"status.editor.error.upload" = "Error uploading";
|
||||
"status.editor.language-select.navigation-title" = "Select Language";
|
||||
"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.image-description" = "Image description";
|
||||
"status.editor.mode.edit" = "Editing your post";
|
||||
|
|
|
@ -376,6 +376,8 @@
|
|||
"status.editor.error.upload" = "Error uploading";
|
||||
"status.editor.language-select.navigation-title" = "Select Language";
|
||||
"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.image-description" = "Image description";
|
||||
"status.editor.mode.edit" = "Editing your post";
|
||||
|
|
|
@ -376,6 +376,8 @@
|
|||
"status.editor.error.upload" = "Error subiendo";
|
||||
"status.editor.language-select.navigation-title" = "Seleccionar idioma";
|
||||
"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.image-description" = "Descripción de la imagen";
|
||||
"status.editor.mode.edit" = "Editando tu publicación";
|
||||
|
|
|
@ -375,6 +375,8 @@
|
|||
"status.editor.error.upload" = "Errorea igotzerakoan";
|
||||
"status.editor.language-select.navigation-title" = "Hautatu hizkuntza";
|
||||
"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.image-description" = "Irudiaren deskribapena";
|
||||
"status.editor.mode.edit" = "Bidalketa editatzen";
|
||||
|
|
|
@ -371,6 +371,8 @@
|
|||
"status.editor.error.upload" = "Erreur de téléchargement";
|
||||
"status.editor.language-select.navigation-title" = "Sélectionner la langue";
|
||||
"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.image-description" = "Description de l'image";
|
||||
"status.editor.mode.edit" = "Modification de votre publication";
|
||||
|
|
|
@ -376,6 +376,8 @@
|
|||
"status.editor.error.upload" = "Errore durante il caricamento";
|
||||
"status.editor.language-select.navigation-title" = "Scegli la lingua";
|
||||
"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.image-description" = "Descrizione dell'immagine";
|
||||
"status.editor.mode.edit" = "Modifica post";
|
||||
|
|
|
@ -375,6 +375,8 @@
|
|||
"status.editor.error.upload" = "アップロードエラー";
|
||||
"status.editor.language-select.navigation-title" = "言語設定";
|
||||
"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.image-description" = "イメージの説明文";
|
||||
"status.editor.mode.edit" = "投稿を編集する";
|
||||
|
|
|
@ -377,6 +377,8 @@
|
|||
"status.editor.error.upload" = "전송 오류";
|
||||
"status.editor.language-select.navigation-title" = "언어 선택";
|
||||
"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.image-description" = "이미지 설명";
|
||||
"status.editor.mode.edit" = "글 수정";
|
||||
|
|
|
@ -375,6 +375,8 @@
|
|||
"status.editor.error.upload" = "Feil ved opplasting";
|
||||
"status.editor.language-select.navigation-title" = "Velg språk";
|
||||
"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.image-description" = "Bildebeskrivelse";
|
||||
"status.editor.mode.edit" = "Redigerer innlegget ditt";
|
||||
|
|
|
@ -369,6 +369,8 @@
|
|||
"status.editor.error.upload" = "Fout tijdens uploaden";
|
||||
"status.editor.language-select.navigation-title" = "Taal selecteren";
|
||||
"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.image-description" = "Omschrijving";
|
||||
"status.editor.mode.edit" = "Post bewerken";
|
||||
|
|
|
@ -371,6 +371,8 @@
|
|||
"status.editor.error.upload" = "Błąd wysyłania";
|
||||
"status.editor.language-select.navigation-title" = "Wybierz język";
|
||||
"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.image-description" = "Opis obrazka";
|
||||
"status.editor.mode.edit" = "Edycja twojego postu";
|
||||
|
|
|
@ -375,6 +375,8 @@
|
|||
"status.editor.error.upload" = "Erro ao fazer upload";
|
||||
"status.editor.language-select.navigation-title" = "Selecionar Idioma";
|
||||
"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.image-description" = "Descrição da imagem";
|
||||
"status.editor.mode.edit" = "Editando sua postagem";
|
||||
|
|
|
@ -371,6 +371,8 @@
|
|||
"status.editor.error.upload" = "Yüklerken Hata Oluştu";
|
||||
"status.editor.language-select.navigation-title" = "Dil Seç";
|
||||
"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.image-description" = "Görüntü Açıklaması";
|
||||
"status.editor.mode.edit" = "Gönderin Düzenleniyor";
|
||||
|
|
|
@ -376,6 +376,8 @@
|
|||
"status.editor.error.upload" = "上传错误";
|
||||
"status.editor.language-select.navigation-title" = "选择语言";
|
||||
"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.image-description" = "图片描述";
|
||||
"status.editor.mode.edit" = "正在编辑你的嘟文";
|
||||
|
|
|
@ -21,6 +21,7 @@ public struct StatusEditorView: View {
|
|||
@FocusState private var isSpoilerTextFocused: Bool
|
||||
|
||||
@State private var isDismissAlertPresented: Bool = false
|
||||
@State private var isLanguageConfirmPresented = false
|
||||
|
||||
public init(mode: StatusEditorViewModel.Mode) {
|
||||
_viewModel = StateObject(wrappedValue: .init(mode: mode))
|
||||
|
@ -104,12 +105,12 @@ public struct StatusEditorView: View {
|
|||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
Task {
|
||||
let status = await viewModel.postStatus()
|
||||
if status != nil {
|
||||
dismiss()
|
||||
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
|
||||
object: nil)
|
||||
}
|
||||
viewModel.evaluateLanguages()
|
||||
if let _ = viewModel.languageConfirmationDialogLanguages {
|
||||
isLanguageConfirmPresented = true
|
||||
} else {
|
||||
await postStatus()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if viewModel.isPosting {
|
||||
|
@ -120,6 +121,9 @@ public struct StatusEditorView: View {
|
|||
}
|
||||
.disabled(!viewModel.canPost)
|
||||
.keyboardShortcut(.return, modifiers: .command)
|
||||
.confirmationDialog("", isPresented: $isLanguageConfirmPresented, actions: {
|
||||
languageConfirmationDialog
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
|
@ -156,6 +160,42 @@ public struct StatusEditorView: View {
|
|||
.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
|
||||
private var spoilerTextView: some View {
|
||||
if viewModel.spoilerOn {
|
||||
|
|
|
@ -14,6 +14,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
var currentAccount: Account?
|
||||
var theme: Theme?
|
||||
var preferences: UserPreferences?
|
||||
var languageConfirmationDialogLanguages: [String: String]?
|
||||
|
||||
var textView: UITextView? {
|
||||
didSet {
|
||||
|
@ -141,6 +142,17 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
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? {
|
||||
guard let client else { return nil }
|
||||
do {
|
||||
|
@ -153,20 +165,6 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
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,
|
||||
visibility: visibility,
|
||||
inReplyToId: mode.replyToStatus?.id,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -82,9 +82,8 @@ struct StatusRowContextMenu: View {
|
|||
await viewModel.translate(userLang: lang)
|
||||
}
|
||||
} label: {
|
||||
if let statusLang = viewModel.status.language,
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: statusLang)
|
||||
{
|
||||
if let statusLang = viewModel.getStatusLang(),
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: statusLang) {
|
||||
Label("status.action.translate-from-\(languageName)", systemImage: "captions.bubble")
|
||||
} else {
|
||||
Label("status.action.translate", systemImage: "captions.bubble")
|
||||
|
|
|
@ -345,14 +345,24 @@ public struct StatusRowView: View {
|
|||
.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
|
||||
private func makeTranslateView(status: AnyStatus) -> some View {
|
||||
if let userLang = preferences.serverPreferences?.postLanguage,
|
||||
preferences.showTranslateButton,
|
||||
status.language != nil,
|
||||
userLang != status.language,
|
||||
!status.content.asRawText.isEmpty,
|
||||
viewModel.translation == nil
|
||||
if let userLang = preferences.serverPreferences?.postLanguage,
|
||||
shouldShowTranslateButton(status: status)
|
||||
{
|
||||
Button {
|
||||
Task {
|
||||
|
@ -362,13 +372,13 @@ public struct StatusRowView: View {
|
|||
if viewModel.isLoadingTranslation {
|
||||
ProgressView()
|
||||
} else {
|
||||
if let statusLanguage = status.language,
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: statusLanguage)
|
||||
{
|
||||
Text("status.action.translate-from-\(languageName)")
|
||||
} else {
|
||||
Text("status.action.translate")
|
||||
}
|
||||
if let statusLanguage = viewModel.getStatusLang(),
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: statusLanguage)
|
||||
{
|
||||
Text("status.action.translate-from-\(languageName)")
|
||||
} else {
|
||||
Text("status.action.translate")
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
|
|
|
@ -2,6 +2,7 @@ import Env
|
|||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import NaturalLanguage
|
||||
|
||||
import DesignSystem
|
||||
|
||||
|
@ -289,8 +290,16 @@ public class StatusRowViewModel: ObservableObject {
|
|||
reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount
|
||||
repliesCount = status.reblog?.repliesCount ?? status.repliesCount
|
||||
}
|
||||
|
||||
|
||||
func getStatusLang() -> String? {
|
||||
status.language
|
||||
}
|
||||
|
||||
func translate(userLang: String) async {
|
||||
await translate(userLang: userLang, sourceLang: getStatusLang())
|
||||
}
|
||||
|
||||
private func translate(userLang: String, sourceLang: String?) async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
withAnimation {
|
||||
|
|
Loading…
Reference in New Issue