Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7df7ee8

Browse files
authoredDec 10, 2024··
feat(firestore-translate-text): optionally allow genkit for translations (#2228)
* feat(firestore-translate-text): optionally allow the use of Gemini 1.5 Pro for translations * chore(firestore-translate-text): bump extension version * chore(firestore-translate-text): add JSDoc comments to new code
1 parent 45b9791 commit 7df7ee8

File tree

9 files changed

+3929
-430
lines changed

9 files changed

+3929
-430
lines changed
 

‎firestore-translate-text/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## Version 0.1.20
2+
3+
feat - add optional Gemini translations powered by Firebase Genkit
4+
15
## Version 0.1.19
26

37
fixed - bump dependencies, fix vulnerabilities

‎firestore-translate-text/PREINSTALL.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@ of languages, such as `en,fr,de`. See the [supported languages list](https://clo
3333

3434
Before installing this extension, make sure that you've [set up a Cloud Firestore database](https://firebase.google.com/docs/firestore/quickstart) in your Firebase project.
3535

36+
#### Optional Genkit Integration
37+
38+
This extension optionally supports Genkit as an alternative to the Google Cloud Translation API for performing translations. With Genkit, you can leverage large language models such as Google AI Gemini or Vertex AI Gemini to generate translations.
39+
40+
##### How it works:
41+
Genkit Integration allows you to use the powerful Gemini 1.5 Pro model for translations. When enabled, the extension uses the specified Genkit provider to perform the translations instead of the default Cloud Translation API.
42+
43+
You can choose between:
44+
45+
- Google AI: Uses the googleai plugin with an API key.
46+
- Vertex AI: Uses the vertexai plugin and connects to your Google Cloud Vertex AI endpoint.
47+
48+
In theory, a large language model like Gemini 1.5 Pro may have more contextual understanding. For example in the sentence `I left my keys in the bank` the model may understand whether `bank` refers to a financial institution or a riverbank, and may provide a more accurate translation.
49+
50+
##### Notes:
51+
- Using Genkit may incur additional charges based on your model provider (Google AI or Vertex AI).
52+
- If you do not wish to use Genkit, the extension defaults to the Cloud Translation API.
53+
3654
#### Billing
3755
To install an extension, your project must be on the [Blaze (pay as you go) plan](https://firebase.google.com/pricing)
3856

‎firestore-translate-text/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ of languages, such as `en,fr,de`. See the [supported languages list](https://clo
4141

4242
Before installing this extension, make sure that you've [set up a Cloud Firestore database](https://firebase.google.com/docs/firestore/quickstart) in your Firebase project.
4343

44+
#### Optional Genkit Integration
45+
46+
This extension optionally supports Genkit as an alternative to the Google Cloud Translation API for performing translations. With Genkit, you can leverage large language models such as Google AI Gemini or Vertex AI Gemini to generate translations.
47+
48+
##### How it works:
49+
Genkit Integration allows you to use the powerful Gemini 1.5 Pro model for translations. When enabled, the extension uses the specified Genkit provider to perform the translations instead of the default Cloud Translation API.
50+
51+
You can choose between:
52+
53+
- Google AI: Uses the googleai plugin with an API key.
54+
- Vertex AI: Uses the vertexai plugin and connects to your Google Cloud Vertex AI endpoint.
55+
56+
In theory, a large language model like Gemini 1.5 Pro may have more contextual understanding. For example in the sentence `I left my keys in the bank` the model may understand whether `bank` refers to a financial institution or a riverbank, and may provide a more accurate translation.
57+
58+
##### Notes:
59+
- Using Genkit may incur additional charges based on your model provider (Google AI or Vertex AI).
60+
- If you do not wish to use Genkit, the extension defaults to the Cloud Translation API.
61+
4462
#### Billing
4563
To install an extension, your project must be on the [Blaze (pay as you go) plan](https://firebase.google.com/pricing)
4664

@@ -70,6 +88,15 @@ To install an extension, your project must be on the [Blaze (pay as you go) plan
7088
* Languages field name: What is the name of the field that contains the languages that you want to translate into? This field is optional. If you don't specify it, the extension will use the languages specified in the LANGUAGES parameter.
7189

7290

91+
* Use Genkit for translations?: If you want to use Genkit to perform translations, select "Yes" and provide the necessary configuration parameters. If you select "No", the extension will use Google Cloud Translation API.
92+
93+
94+
* Genkit Gemini provider: If you selected to use Genkit to perform translations, please provide the name of the Gemini API you want to use.
95+
96+
97+
* Google AI API key: If you selected to use Genkit with Google AI to perform translations, please provide a Google AI API key
98+
99+
73100

74101

75102
**Cloud Functions:**
@@ -84,6 +111,8 @@ To install an extension, your project must be on the [Blaze (pay as you go) plan
84111

85112
* translate.googleapis.com (Reason: To use Google Translate to translate strings into your specified target languages.)
86113

114+
* aiplatform.googleapis.com (Reason: This extension uses the Vertex AI multimodal model for embedding images, if configured to do so.)
115+
87116

88117

89118
**Access Required**:
@@ -93,3 +122,5 @@ To install an extension, your project must be on the [Blaze (pay as you go) plan
93122
This extension will operate with the following project IAM roles:
94123

95124
* datastore.user (Reason: Allows the extension to write translated strings to Cloud Firestore.)
125+
126+
* aiplatform.user (Reason: This extension requires access to Vertex AI to create, update and query a Vertex Matching Engine index.)

‎firestore-translate-text/extension.yaml

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
name: firestore-translate-text
16-
version: 0.1.19
16+
version: 0.1.20
1717
specVersion: v1beta
1818

1919
tags: [ai]
@@ -47,11 +47,20 @@ apis:
4747
reason:
4848
To use Google Translate to translate strings into your specified target
4949
languages.
50+
- apiName: aiplatform.googleapis.com
51+
reason:
52+
This extension uses the Vertex AI multimodal model for embedding images,
53+
if configured to do so.
5054

5155
roles:
5256
- role: datastore.user
5357
reason: Allows the extension to write translated strings to Cloud Firestore.
5458

59+
- role: aiplatform.user
60+
reason: >-
61+
This extension requires access to Vertex AI to create, update and query a
62+
Vertex Matching Engine index.
63+
5564
resources:
5665
- name: fstranslate
5766
type: firebaseextensions.v1beta.function
@@ -130,6 +139,41 @@ params:
130139
default: languages
131140
required: false
132141

142+
- param: USE_GENKIT
143+
label: Use Genkit for translations?
144+
description: >
145+
If you want to use Genkit to perform translations, select "Yes" and
146+
provide the necessary configuration parameters. If you select "No", the
147+
extension will use Google Cloud Translation API.
148+
type: select
149+
required: true
150+
options:
151+
- label: Yes
152+
value: true
153+
- label: No
154+
value: false
155+
156+
- param: GEMINI_PROVIDER
157+
label: Genkit Gemini provider
158+
description: >
159+
If you selected to use Genkit to perform translations, please provide the
160+
name of the Gemini API you want to use.
161+
type: select
162+
required: false
163+
options:
164+
- label: Google AI
165+
value: googleai
166+
- label: Vertex AI
167+
value: vertexai
168+
169+
- param: GOOGLE_AI_API_KEY
170+
label: Google AI API key
171+
description: >
172+
If you selected to use Genkit with Google AI to perform translations,
173+
please provide a Google AI API key
174+
type: secret
175+
required: false
176+
133177
# - param: DO_BACKFILL
134178
# label: Translate existing documents?
135179
# description: >

‎firestore-translate-text/functions/package-lock.json

Lines changed: 3586 additions & 363 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎firestore-translate-text/functions/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,24 @@
1212
"generate-readme": "firebase ext:info .. --markdown > ../README.md"
1313
},
1414
"dependencies": {
15+
"@genkit-ai/googleai": "^0.9.7",
16+
"@genkit-ai/vertexai": "^0.9.7",
1517
"@google-cloud/translate": "^8.2.0",
18+
"@google-cloud/vertexai": "^1.9.2",
1619
"@types/express-serve-static-core": "4.19.0",
1720
"@types/node": "^20.10.3",
1821
"firebase-admin": "^12.1.0",
1922
"firebase-functions": "^4.9.0",
23+
"genkit": "^0.9.7",
2024
"rimraf": "^2.6.3",
2125
"typescript": "^4.8.4"
2226
},
2327
"devDependencies": {
28+
"@types/jest": "29.5.0",
2429
"firebase-functions-test": "3.2.0",
30+
"jest": "29.5.0",
2531
"js-yaml": "^3.13.1",
2632
"mocked-env": "^1.3.1",
27-
"@types/jest": "29.5.0",
28-
"jest": "29.5.0",
2933
"ts-jest": "29.1.2"
3034
},
3135
"private": true

‎firestore-translate-text/functions/src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ export default {
2121
inputFieldName: process.env.INPUT_FIELD_NAME,
2222
outputFieldName: process.env.OUTPUT_FIELD_NAME,
2323
languagesFieldName: process.env.LANGUAGES_FIELD_NAME,
24+
useGenkit: process.env.USE_GENKIT === "true",
25+
geminiProvider: process.env.GEMINI_PROVIDER,
26+
googleAIAPIKey: process.env.GOOGLE_API_KEY,
2427
};

‎firestore-translate-text/functions/src/translate/common.ts

Lines changed: 234 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,250 @@ import * as logs from "../logs";
33
import * as events from "../events";
44
import * as admin from "firebase-admin";
55
import config from "../config";
6+
import { genkit, Genkit, z, ModelReference } from "genkit";
7+
import vertexAI, {
8+
gemini15Pro as gemini15ProVertex,
9+
} from "@genkit-ai/vertexai";
10+
import {
11+
gemini15Pro as gemini15ProGoogleAI,
12+
googleAI,
13+
} from "@genkit-ai/googleai";
614

15+
/**
16+
* Represents a translation result with target language and translated text
17+
*/
718
export type Translation = {
819
language: string;
920
output: string;
1021
};
1122

12-
export const translate = new v2.Translate({
13-
projectId: process.env.PROJECT_ID,
14-
});
15-
16-
export const translateString = async (
17-
string: string,
18-
targetLanguage: string
19-
): Promise<string> => {
20-
try {
21-
const [translatedString] = await translate.translate(
22-
string,
23-
targetLanguage
24-
);
25-
logs.translateStringComplete(string, targetLanguage, translatedString);
26-
return translatedString;
27-
} catch (err) {
28-
logs.translateStringError(string, targetLanguage, err);
29-
await events.recordErrorEvent(err as Error);
30-
throw err;
23+
/**
24+
* Interface defining the contract for translator implementations
25+
*/
26+
interface ITranslator {
27+
/**
28+
* Translates text to a target language
29+
* @param text - The text to translate
30+
* @param targetLanguage - The language code to translate to
31+
* @returns A promise resolving to the translated text
32+
*/
33+
translate(text: string, targetLanguage: string): Promise<string>;
34+
}
35+
36+
/**
37+
* Implementation of ITranslator using Google Cloud Translation API v2
38+
*/
39+
export class GoogleTranslator implements ITranslator {
40+
private client: v2.Translate;
41+
42+
/**
43+
* Creates a new instance of GoogleTranslator
44+
* @param projectId - The Google Cloud project ID
45+
*/
46+
constructor(projectId: string) {
47+
this.client = new v2.Translate({ projectId });
3148
}
32-
};
3349

34-
export const extractInput = (
35-
snapshot: admin.firestore.DocumentSnapshot
36-
): any => {
37-
return snapshot.get(config.inputFieldName);
38-
};
50+
/**
51+
* Translates text using Google Cloud Translation API
52+
* @param text - The text to translate
53+
* @param targetLanguage - The language code to translate to
54+
* @returns A promise resolving to the translated text
55+
* @throws Will throw an error if translation fails
56+
*/
57+
async translate(text: string, targetLanguage: string): Promise<string> {
58+
try {
59+
const [translatedString] = await this.client.translate(
60+
text,
61+
targetLanguage
62+
);
63+
logs.translateStringComplete(text, targetLanguage, translatedString);
64+
return translatedString;
65+
} catch (err) {
66+
logs.translateStringError(text, targetLanguage, err);
67+
await events.recordErrorEvent(err as Error);
68+
throw err;
69+
}
70+
}
71+
}
3972

40-
export const extractOutput = (
41-
snapshot: admin.firestore.DocumentSnapshot
42-
): any => {
43-
return snapshot.get(config.outputFieldName);
44-
};
73+
/**
74+
* Implementation of ITranslator using Genkit with either Vertex AI or Google AI
75+
*/
76+
export class GenkitTranslator implements ITranslator {
77+
private client: Genkit;
78+
plugin: "vertexai" | "googleai";
79+
model: ModelReference<any>;
4580

46-
export const extractLanguages = (
47-
snapshot: admin.firestore.DocumentSnapshot
48-
): string[] => {
49-
if (!config.languagesFieldName) return config.languages;
50-
return snapshot.get(config.languagesFieldName) || config.languages;
51-
};
81+
/**
82+
* Creates a new instance of GenkitTranslator
83+
* @param options - Configuration options for the translator
84+
* @param options.plugin - The AI service provider to use ("vertexai" or "googleai")
85+
* @throws Will throw an error if required API keys are missing
86+
*/
87+
constructor({ plugin }: { plugin: "vertexai" | "googleai" }) {
88+
this.plugin = plugin;
89+
if (plugin === "googleai" && !config.googleAIAPIKey) {
90+
throw new Error(
91+
"Google AI API key is required for Genkit Google AI translations"
92+
);
93+
}
5294

53-
export const filterLanguagesFn = (
54-
existingTranslations: Record<string, any>
55-
): ((targetLanguage: string) => boolean) => {
56-
return (targetLanguage: string) => {
57-
if (existingTranslations[targetLanguage] != undefined) {
58-
logs.skippingLanguage(targetLanguage);
59-
return false;
95+
this.model =
96+
plugin === "vertexai" ? gemini15ProVertex : gemini15ProGoogleAI;
97+
98+
const plugins =
99+
plugin === "vertexai"
100+
? [vertexAI({ location: process.env.LOCATION! })]
101+
: [googleAI({ apiKey: config.googleAIAPIKey })];
102+
103+
this.client = genkit({
104+
plugins,
105+
});
106+
}
107+
108+
/**
109+
* Translates text using Genkit with either Vertex AI or Google AI
110+
* @param text - The text to translate
111+
* @param targetLanguage - The language code to translate to
112+
* @returns A promise resolving to the translated text
113+
* @throws Will throw an error if translation fails or no output is returned
114+
*/
115+
async translate(text: string, targetLanguage: string): Promise<string> {
116+
try {
117+
const prompt =
118+
"Translate the following text to " + targetLanguage + ":\n" + text;
119+
120+
const response = await this.client.generate({
121+
model: this.model,
122+
output: {
123+
format: "json",
124+
schema: z.object({
125+
translation: z.string(),
126+
}),
127+
},
128+
prompt: prompt,
129+
});
130+
131+
if (!response.output) {
132+
throw new Error("No translation returned from Gemini 1.5 Pro");
133+
}
134+
135+
logs.translateStringComplete(text, targetLanguage, response.text);
136+
return response.output.translation;
137+
} catch (err) {
138+
logs.translateStringError(text, targetLanguage, err);
139+
await events.recordErrorEvent(err as Error);
140+
throw err;
60141
}
61-
return true;
62-
};
63-
};
142+
}
143+
}
64144

65-
export const updateTranslations = async (
66-
snapshot: admin.firestore.DocumentSnapshot,
67-
translations: any
68-
): Promise<void> => {
69-
logs.updateDocument(snapshot.ref.path);
70-
// Wrapping in transaction to allow for automatic retries (#48)
71-
await admin.firestore().runTransaction((transaction) => {
72-
transaction.update(snapshot.ref, config.outputFieldName, translations);
73-
return Promise.resolve();
74-
});
75-
76-
logs.updateDocumentComplete(snapshot.ref.path);
77-
await events.recordSuccessEvent({
78-
subject: snapshot.ref.path,
79-
data: { outputFieldName: config.outputFieldName, translations },
80-
});
81-
};
145+
/**
146+
* Service class that orchestrates translation operations using the provided translator
147+
*/
148+
export class TranslationService {
149+
/**
150+
* Creates a new instance of TranslationService
151+
* @param translator - The translator implementation to use
152+
*/
153+
constructor(private translator: ITranslator) {}
154+
155+
/**
156+
* Translates a string to the specified target language
157+
* @param text - The text to translate
158+
* @param targetLanguage - The language code to translate to
159+
* @returns A promise resolving to the translated text
160+
*/
161+
async translateString(text: string, targetLanguage: string): Promise<string> {
162+
return this.translator.translate(text, targetLanguage);
163+
}
164+
165+
/**
166+
* Extracts input field value from a Firestore document
167+
* @param snapshot - The Firestore document snapshot
168+
* @returns The value of the configured input field
169+
*/
170+
extractInput(snapshot: admin.firestore.DocumentSnapshot): any {
171+
return snapshot.get(config.inputFieldName);
172+
}
173+
174+
/**
175+
* Extracts output field value from a Firestore document
176+
* @param snapshot - The Firestore document snapshot
177+
* @returns The value of the configured output field
178+
*/
179+
extractOutput(snapshot: admin.firestore.DocumentSnapshot): any {
180+
return snapshot.get(config.outputFieldName);
181+
}
182+
183+
/**
184+
* Extracts target languages from a Firestore document or returns default languages
185+
* @param snapshot - The Firestore document snapshot
186+
* @returns Array of language codes to translate to
187+
*/
188+
extractLanguages(snapshot: admin.firestore.DocumentSnapshot): string[] {
189+
if (!config.languagesFieldName) return config.languages;
190+
return snapshot.get(config.languagesFieldName) || config.languages;
191+
}
192+
193+
/**
194+
* Creates a filter function to skip already translated languages
195+
* @param existingTranslations - Record of existing translations
196+
* @returns A function that returns true for languages that need translation
197+
*/
198+
filterLanguagesFn(
199+
existingTranslations: Record<string, any>
200+
): (targetLanguage: string) => boolean {
201+
return (targetLanguage: string) => {
202+
if (existingTranslations[targetLanguage] != undefined) {
203+
logs.skippingLanguage(targetLanguage);
204+
return false;
205+
}
206+
return true;
207+
};
208+
}
209+
210+
/**
211+
* Updates translations in a Firestore document
212+
* @param snapshot - The Firestore document snapshot
213+
* @param translations - The translations to update
214+
* @returns A promise that resolves when the update is complete
215+
*/
216+
async updateTranslations(
217+
snapshot: admin.firestore.DocumentSnapshot,
218+
translations: any
219+
): Promise<void> {
220+
logs.updateDocument(snapshot.ref.path);
221+
222+
await admin.firestore().runTransaction((transaction) => {
223+
transaction.update(snapshot.ref, config.outputFieldName, translations);
224+
return Promise.resolve();
225+
});
226+
227+
logs.updateDocumentComplete(snapshot.ref.path);
228+
await events.recordSuccessEvent({
229+
subject: snapshot.ref.path,
230+
data: { outputFieldName: config.outputFieldName, translations },
231+
});
232+
}
233+
}
234+
235+
// Initialize the translation service based on configuration
236+
const translationService = config.useGenkit
237+
? new TranslationService(new GenkitTranslator({ plugin: "vertexai" }))
238+
: new TranslationService(new GoogleTranslator(process.env.PROJECT_ID));
239+
240+
// Export bound methods for convenience
241+
export const translateString =
242+
translationService.translateString.bind(translationService);
243+
export const extractInput =
244+
translationService.extractInput.bind(translationService);
245+
export const extractOutput =
246+
translationService.extractOutput.bind(translationService);
247+
export const extractLanguages =
248+
translationService.extractLanguages.bind(translationService);
249+
export const filterLanguagesFn =
250+
translationService.filterLanguagesFn.bind(translationService);
251+
export const updateTranslations =
252+
translationService.updateTranslations.bind(translationService);

‎firestore-translate-text/functions/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"module": "commonjs",
66
"noImplicitReturns": true,
77
"sourceMap": false,
8-
"outDir": "lib"
8+
"outDir": "lib",
9+
"skipLibCheck": true
910
},
1011
"compileOnSave": true,
1112
"include": ["src"]

0 commit comments

Comments
 (0)
Please sign in to comment.