Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a language management feature to support change application language and multi-language message translations #31

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ android {
applicationId "com.suesitran.publicchat"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
minSdkVersion 23
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
142 changes: 70 additions & 72 deletions functions/index.js
Original file line number Diff line number Diff line change
@@ -1,95 +1,93 @@
/**
* Import function triggers from their respective submodules:
*
* const {onCall} = require("firebase-functions/v2/https");
* const {onDocumentWritten} = require("firebase-functions/v2/firestore");
*
* See a full list of supported triggers at https://firebase.google.com/docs/functions
*/
const v2 = require("firebase-functions/v2");
const vertexAIApi = require("@google-cloud/vertexai");
const admin = require("firebase-admin");

// Create and deploy your first functions
// https://firebase.google.com/docs/functions/get-started
admin.initializeApp();

const project = 'proj-atc';
const location = 'us-central1';
const textModel = 'gemini-1.5-flash';
const textModel = 'gemini-1.5-flash';
const visionModel = 'gemini-1.0-pro-vision';

const vertexAI = new vertexAIApi.VertexAI({project: project, location: location});

const generativeVisionModel = vertexAI.getGenerativeModel({
model: visionModel,
});
const vertexAI = new vertexAIApi.VertexAI({ project: project, location: location });

const generativeModelPreview = vertexAI.preview.getGenerativeModel({
model: textModel,
model: textModel,
});

const generationConfig = {
temperature: 1,
topP: 0.95,
topK: 64,
maxOutputTokens: 8192,
responseMimeType: "application/json",
responseSchema: {
type: "object",
properties: {
en: {
type: "string"
}
},
required: [
"en"
]
},
};

// use onDocumentWritten here to prepare to "edit message" feature later
exports.onChatWritten = v2.firestore.onDocumentWritten("/public/{messageId}",async (event) => {
const document = event.data.after.data();
const message = document["message"];
console.log(`message: ${message}`);

// no message? do nothing
if (message == undefined) {
return;
}
const curTranslated = document["translated"];

// check if message is translated
if (curTranslated != undefined) {
// message is translated before,
// check the original message
const original = curTranslated["original"];

console.log('Original: ', original);
// message is same as original, meaning it's already translated. Do nothing
if (message == original) {
return;
}
// trigger when the chat data on public collection change to translate the message
exports.onChatWritten_001 = v2.firestore.onDocumentWritten("/public/{messageId}", async (event) => {
const document = event.data.after.data();
const message = document["message"];

console.log(`message: ${message}`);

// no message? do nothing
if (message == undefined) {
return;
}

const curTranslated = document["translated"];

// check if message is translated
if (curTranslated != undefined) {
// message is translated before,
// check the original message
const original = curTranslated["original"];

console.log('Original: ', original);
// message is same as original, meaning it's already translated. Do nothing
if (message == original) {
return;
}
}

//get all languages list to translate the message
const db = admin.firestore();
const languagesCollection = db.collection("languages");
const languages = await languagesCollection.get();
const languageCodes = languages.docs.map((e) => e.data().code)

var translated = {}

if (languageCodes.length > 0) {
const generationConfig = {
temperature: 1,
topP: 0.95,
topK: 64,
maxOutputTokens: 8192,
responseMimeType: "application/json",
responseSchema: {
type: "object",
properties: languageCodes.reduce((acc, lang) => {
acc[lang] = { type: "string" };
return acc;
}, {}),
required: languageCodes
},
};

const chatSession = generativeModelPreview.startChat({
generationConfig: generationConfig
generationConfig: generationConfig
});
const result = await chatSession.sendMessage(`translate this text to English: ${message}`);

const result = await chatSession.sendMessage(`translate this text [${languageCodes.join(", ")}]: ${message}`);
const response = result.response;
console.log('Response:', JSON.stringify(response));

const jsonTranslated = response.candidates[0].content.parts[0].text;
console.log('translated json: ', jsonTranslated);
// parse this json to get translated text out
const translated = JSON.parse(jsonTranslated);
console.log('final result: ', translated.en);

// write to message
const data = event.data.after.data();
return event.data.after.ref.set({
'translated': {
'original':message,
'en': translated.en
}
}, {merge: true});
translated = JSON.parse(jsonTranslated);
console.log('final result: ', translated);

}

// write to message
return event.data.after.ref.set({
'translated': {
'original': message,
...translated,
}
}, { merge: true });
})
6 changes: 3 additions & 3 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -477,7 +477,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -528,7 +528,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down
37 changes: 37 additions & 0 deletions lib/_shared/bloc/language_cubit.dart/language_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:equatable/equatable.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:public_chat/_shared/data/language.dart';
import 'package:public_chat/repository/database.dart';
import 'package:public_chat/service_locator/service_locator.dart';
import 'package:public_chat/utils/bloc_extensions.dart';
import 'package:public_chat/utils/locale_support.dart';

part 'language_state.dart';

class LanguageCubit extends Cubit<LanguageState> {
LanguageCubit() : super(const LanguageState());

void init(BuildContext context) {
setAppLanguage(Language.fromMapping(code: context.locale.localeName));
}

void setAppLanguage(Language language) {
emitSafely(state.copyWith(appLanguage: language));
}

void setMessageLanguage(Language? language, {bool saveToDatabase = true}) {
if (saveToDatabase) {
final user = FirebaseAuth.instance.currentUser;

if (user != null) {
final database = ServiceLocator.instance.get<Database>();
database.saveUser(user, language);
if (language != null) database.addLanguage(language);
}
}

emitSafely(state.copyWith(messageLanguage: language));
}
}
22 changes: 22 additions & 0 deletions lib/_shared/bloc/language_cubit.dart/language_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
part of 'language_cubit.dart';

final class LanguageState extends Equatable {
final Language? appLanguage;
final Language? messageLanguage;

const LanguageState({
this.appLanguage,
this.messageLanguage,
});

LanguageState copyWith({
Language? appLanguage,
Language? messageLanguage,
}) =>
LanguageState(
appLanguage: appLanguage ?? this.appLanguage,
messageLanguage: messageLanguage ?? this.messageLanguage);

@override
List<Object?> get props => [appLanguage, messageLanguage];
}
40 changes: 33 additions & 7 deletions lib/_shared/data/chat_data.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:public_chat/_shared/data/language.dart';

final class Message {
final String id;
Expand All @@ -23,20 +25,44 @@ final class Message {
{'message': message, 'sender': sender, 'time': timestamp};
}

final class UserDetail {
final class UserDetail extends Equatable {
final String displayName;
final String? photoUrl;
final String uid;
final Language? messageLanguage;

UserDetail.fromFirebaseUser(User user)
const UserDetail({
required this.uid,
required this.displayName,
required this.photoUrl,
required this.messageLanguage,
});

UserDetail.fromFirebaseUser(User user, [this.messageLanguage])
: displayName = user.displayName ?? 'Unknown',
photoUrl = user.photoURL,
uid = user.uid;

UserDetail.fromMap(this.uid, Map<String, dynamic> map)
: displayName = map['displayName'],
photoUrl = map['photoUrl'];
factory UserDetail.fromMap(String uid, Map<String, dynamic> map) {
final messageLanguageData = map['messageLanguage'];

Map<String, dynamic> toMap() =>
{'displayName': displayName, 'photoUrl': photoUrl};
return UserDetail(
uid: uid,
displayName: map['displayName'],
photoUrl: map['photoUrl'],
messageLanguage: messageLanguageData is Map
? Language.fromMap(messageLanguageData)
: null,
);
}

Map<String, dynamic> toMap() => {
'displayName': displayName,
'photoUrl': photoUrl,
if (messageLanguage != null)
'messageLanguage': messageLanguage!.toMap(importCountry: false)
};

@override
List<Object?> get props => [displayName, photoUrl, uid, messageLanguage];
}
23 changes: 23 additions & 0 deletions lib/_shared/data/country.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:equatable/equatable.dart';

final class Country extends Equatable {
final String name;
final String code;

const Country({required this.name, required this.code});

factory Country.fromMap(Map json) => Country(
code: json['name'],
name: json['code'],
);

factory Country.fromRestCountries(Map json) => Country(
code: json['cca2'],
name: json['name']['common'],
);

Map<String, dynamic> toMap() => {'name': name, 'code': code};

@override
List<Object?> get props => [name, code];
}
Loading