Skip to content

Commit a91c724

Browse files
authored
feat(backend): add Google Cloud Translation API with EU data residency support (#406)
Implement GDPR-compliant Google Cloud Translation API integration with configurable regional endpoints to ensure data residency requirements. Changes: - Add GOOGLE_CLOUD_TRANSLATION_ENDPOINT config parameter to support regional API endpoints (e.g., translate-eu.googleapis.com) - Set EU defaults: europe-west1 location and translate-eu.googleapis.com endpoint for GDPR compliance by default - Update googleCloudAuth.ts to pass apiEndpoint option to TranslationServiceClient constructor - Configure docker-compose-production.yml to mount Google Cloud service account credentials via volume (./<name of service account>.json) - Update both api and math-updater services to initialize translation client with endpoint configuration - Add comprehensive documentation in env.example for new configuration options The translation service is optional and only enabled when credentials are provided. Both endpoint and location can be overridden via environment variables if different regions are needed. Technical details: - Uses Google Cloud Translation Advanced API (v3) - Supports both AWS Secrets Manager (production) and local file (development) authentication methods - Credentials mounted read-only for security - Enhanced logging to display active endpoint configuration
1 parent 6990cf4 commit a91c724

10 files changed

Lines changed: 57 additions & 14 deletions

File tree

script/docker-compose-production.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ services:
3030
TWILIO_AUTH_TOKEN: "CHANGEME"
3131
TWILIO_SERVICE_SID: "CHANGEME"
3232
SERVER_DID_PROD: "did:web:agoracitizen.app"
33+
GOOGLE_APPLICATION_CREDENTIALS: "/secrets/service-account.json"
34+
volumes:
35+
- ./<name of service account>.json:/secrets/service-account.json:ro
3336
expose:
3437
- "8080"
3538
restart: unless-stopped
@@ -41,6 +44,9 @@ services:
4144
NODE_ENV: "production"
4245
POLIS_BASE_URL: "http://polis-server:8004"
4346
TOTAL_VCPUS: "2" # t3.medium = 2 vCPUs (adjust for your instance type)
47+
GOOGLE_APPLICATION_CREDENTIALS: "/secrets/service-account.json"
48+
volumes:
49+
- ./<name of service account>.json:/secrets/service-account.json:ro
4450
restart: unless-stopped
4551
logging:
4652
driver: awslogs

services/api/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ if (
306306
config.GOOGLE_APPLICATION_CREDENTIALS,
307307
googleCloudTranslationLocation:
308308
config.GOOGLE_CLOUD_TRANSLATION_LOCATION,
309+
googleCloudTranslationEndpoint:
310+
config.GOOGLE_CLOUD_TRANSLATION_ENDPOINT,
309311
log,
310312
});
311313
log.info("[API] Google Cloud Translation initialized successfully");

services/api/src/shared-backend/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ export const sharedConfigSchema = z.object({
1919
AWS_SECRET_REGION_READ: z.string().optional(),
2020
DB_HOST_READ: z.string().optional(),
2121
DB_PORT_READ: z.coerce.number().int().nonnegative().default(5433),
22-
// Google Cloud Translation
23-
GOOGLE_CLOUD_TRANSLATION_LOCATION: z.string().default("global"),
22+
// Google Cloud Translation (defaults to EU for GDPR compliance)
23+
GOOGLE_CLOUD_TRANSLATION_LOCATION: z.string().default("europe-west1"),
24+
// API endpoint for Google Cloud Translation (defaults to EU endpoint for data residency)
25+
GOOGLE_CLOUD_TRANSLATION_ENDPOINT: z
26+
.string()
27+
.default("translate-eu.googleapis.com"),
2428
// AWS Secret Manager key for Google Cloud service account JSON (production)
2529
// If set, this takes precedence over GOOGLE_APPLICATION_CREDENTIALS
2630
GOOGLE_CLOUD_SERVICE_ACCOUNT_AWS_SECRET_KEY: z.string().optional(),

services/api/src/shared-backend/googleCloudAuth.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ function parseServiceAccountJson(
9696
* @param googleCloudServiceAccountAwsSecretKey - AWS secret key containing service account JSON (optional)
9797
* @param awsSecretRegion - AWS region for Secrets Manager (required if using AWS Secrets Manager)
9898
* @param googleApplicationCredentialsPath - Path to service account JSON file (for local development)
99-
* @param googleCloudTranslationLocation - Translation API location (e.g., "global", "us-central1")
99+
* @param googleCloudTranslationLocation - Translation API location (e.g., "global", "europe-west1")
100+
* @param googleCloudTranslationEndpoint - API endpoint (e.g., "translate-eu.googleapis.com" for EU data residency)
100101
* @param log - Pino or Fastify logger instance
101102
* @returns GoogleCloudCredentials with initialized client and config
102103
* @throws Error if authentication fails or required parameters are missing
@@ -106,12 +107,14 @@ export async function initializeGoogleCloudCredentials({
106107
awsSecretRegion,
107108
googleApplicationCredentialsPath,
108109
googleCloudTranslationLocation,
110+
googleCloudTranslationEndpoint,
109111
log,
110112
}: {
111113
googleCloudServiceAccountAwsSecretKey?: string;
112114
awsSecretRegion?: string;
113115
googleApplicationCredentialsPath?: string;
114116
googleCloudTranslationLocation: string;
117+
googleCloudTranslationEndpoint?: string;
115118
log: pino.Logger | FastifyBaseLogger;
116119
}): Promise<GoogleCloudCredentials> {
117120
let serviceAccount: ServiceAccountCredentials;
@@ -165,10 +168,13 @@ export async function initializeGoogleCloudCredentials({
165168
client_email: serviceAccount.client_email,
166169
private_key: serviceAccount.private_key,
167170
},
171+
...(googleCloudTranslationEndpoint && {
172+
apiEndpoint: googleCloudTranslationEndpoint,
173+
}),
168174
});
169175

170176
log.info(
171-
`[Google Cloud Auth] Initialized Translation client for project: ${serviceAccount.project_id}`,
177+
`[Google Cloud Auth] Initialized Translation client for project: ${serviceAccount.project_id}${googleCloudTranslationEndpoint ? ` with endpoint: ${googleCloudTranslationEndpoint}` : ""}`,
172178
);
173179

174180
return {

services/math-updater/env.example

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ AWS_AI_LABEL_SUMMARY_MAX_TOKENS=8192
3030
# AWS_AI_LABEL_SUMMARY_PROMPT="Your custom prompt here..."
3131

3232
# Google Cloud Translation API Configuration
33-
# Optional: Translation API location (default: "global")
34-
# GOOGLE_CLOUD_TRANSLATION_LOCATION=global
33+
# Optional: Translation API location (default: "europe-west1" for EU data residency)
34+
# GOOGLE_CLOUD_TRANSLATION_LOCATION=europe-west1
35+
36+
# Optional: API endpoint (default: "translate-eu.googleapis.com" for EU data residency)
37+
# GOOGLE_CLOUD_TRANSLATION_ENDPOINT=translate-eu.googleapis.com
3538

3639
# Authentication Options (choose one):
3740

services/math-updater/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ async function main() {
9191
config.GOOGLE_APPLICATION_CREDENTIALS,
9292
googleCloudTranslationLocation:
9393
config.GOOGLE_CLOUD_TRANSLATION_LOCATION,
94+
googleCloudTranslationEndpoint:
95+
config.GOOGLE_CLOUD_TRANSLATION_ENDPOINT,
9496
log,
9597
});
9698
log.info(

services/math-updater/src/shared-backend/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ export const sharedConfigSchema = z.object({
1919
AWS_SECRET_REGION_READ: z.string().optional(),
2020
DB_HOST_READ: z.string().optional(),
2121
DB_PORT_READ: z.coerce.number().int().nonnegative().default(5433),
22-
// Google Cloud Translation
23-
GOOGLE_CLOUD_TRANSLATION_LOCATION: z.string().default("global"),
22+
// Google Cloud Translation (defaults to EU for GDPR compliance)
23+
GOOGLE_CLOUD_TRANSLATION_LOCATION: z.string().default("europe-west1"),
24+
// API endpoint for Google Cloud Translation (defaults to EU endpoint for data residency)
25+
GOOGLE_CLOUD_TRANSLATION_ENDPOINT: z
26+
.string()
27+
.default("translate-eu.googleapis.com"),
2428
// AWS Secret Manager key for Google Cloud service account JSON (production)
2529
// If set, this takes precedence over GOOGLE_APPLICATION_CREDENTIALS
2630
GOOGLE_CLOUD_SERVICE_ACCOUNT_AWS_SECRET_KEY: z.string().optional(),

services/math-updater/src/shared-backend/googleCloudAuth.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ function parseServiceAccountJson(
9696
* @param googleCloudServiceAccountAwsSecretKey - AWS secret key containing service account JSON (optional)
9797
* @param awsSecretRegion - AWS region for Secrets Manager (required if using AWS Secrets Manager)
9898
* @param googleApplicationCredentialsPath - Path to service account JSON file (for local development)
99-
* @param googleCloudTranslationLocation - Translation API location (e.g., "global", "us-central1")
99+
* @param googleCloudTranslationLocation - Translation API location (e.g., "global", "europe-west1")
100+
* @param googleCloudTranslationEndpoint - API endpoint (e.g., "translate-eu.googleapis.com" for EU data residency)
100101
* @param log - Pino or Fastify logger instance
101102
* @returns GoogleCloudCredentials with initialized client and config
102103
* @throws Error if authentication fails or required parameters are missing
@@ -106,12 +107,14 @@ export async function initializeGoogleCloudCredentials({
106107
awsSecretRegion,
107108
googleApplicationCredentialsPath,
108109
googleCloudTranslationLocation,
110+
googleCloudTranslationEndpoint,
109111
log,
110112
}: {
111113
googleCloudServiceAccountAwsSecretKey?: string;
112114
awsSecretRegion?: string;
113115
googleApplicationCredentialsPath?: string;
114116
googleCloudTranslationLocation: string;
117+
googleCloudTranslationEndpoint?: string;
115118
log: pino.Logger | FastifyBaseLogger;
116119
}): Promise<GoogleCloudCredentials> {
117120
let serviceAccount: ServiceAccountCredentials;
@@ -165,10 +168,13 @@ export async function initializeGoogleCloudCredentials({
165168
client_email: serviceAccount.client_email,
166169
private_key: serviceAccount.private_key,
167170
},
171+
...(googleCloudTranslationEndpoint && {
172+
apiEndpoint: googleCloudTranslationEndpoint,
173+
}),
168174
});
169175

170176
log.info(
171-
`[Google Cloud Auth] Initialized Translation client for project: ${serviceAccount.project_id}`,
177+
`[Google Cloud Auth] Initialized Translation client for project: ${serviceAccount.project_id}${googleCloudTranslationEndpoint ? ` with endpoint: ${googleCloudTranslationEndpoint}` : ""}`,
172178
);
173179

174180
return {

services/shared-backend/src/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ export const sharedConfigSchema = z.object({
1818
AWS_SECRET_REGION_READ: z.string().optional(),
1919
DB_HOST_READ: z.string().optional(),
2020
DB_PORT_READ: z.coerce.number().int().nonnegative().default(5433),
21-
// Google Cloud Translation
22-
GOOGLE_CLOUD_TRANSLATION_LOCATION: z.string().default("global"),
21+
// Google Cloud Translation (defaults to EU for GDPR compliance)
22+
GOOGLE_CLOUD_TRANSLATION_LOCATION: z.string().default("europe-west1"),
23+
// API endpoint for Google Cloud Translation (defaults to EU endpoint for data residency)
24+
GOOGLE_CLOUD_TRANSLATION_ENDPOINT: z
25+
.string()
26+
.default("translate-eu.googleapis.com"),
2327
// AWS Secret Manager key for Google Cloud service account JSON (production)
2428
// If set, this takes precedence over GOOGLE_APPLICATION_CREDENTIALS
2529
GOOGLE_CLOUD_SERVICE_ACCOUNT_AWS_SECRET_KEY: z.string().optional(),

services/shared-backend/src/googleCloudAuth.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ function parseServiceAccountJson(
9595
* @param googleCloudServiceAccountAwsSecretKey - AWS secret key containing service account JSON (optional)
9696
* @param awsSecretRegion - AWS region for Secrets Manager (required if using AWS Secrets Manager)
9797
* @param googleApplicationCredentialsPath - Path to service account JSON file (for local development)
98-
* @param googleCloudTranslationLocation - Translation API location (e.g., "global", "us-central1")
98+
* @param googleCloudTranslationLocation - Translation API location (e.g., "global", "europe-west1")
99+
* @param googleCloudTranslationEndpoint - API endpoint (e.g., "translate-eu.googleapis.com" for EU data residency)
99100
* @param log - Pino or Fastify logger instance
100101
* @returns GoogleCloudCredentials with initialized client and config
101102
* @throws Error if authentication fails or required parameters are missing
@@ -105,12 +106,14 @@ export async function initializeGoogleCloudCredentials({
105106
awsSecretRegion,
106107
googleApplicationCredentialsPath,
107108
googleCloudTranslationLocation,
109+
googleCloudTranslationEndpoint,
108110
log,
109111
}: {
110112
googleCloudServiceAccountAwsSecretKey?: string;
111113
awsSecretRegion?: string;
112114
googleApplicationCredentialsPath?: string;
113115
googleCloudTranslationLocation: string;
116+
googleCloudTranslationEndpoint?: string;
114117
log: pino.Logger | FastifyBaseLogger;
115118
}): Promise<GoogleCloudCredentials> {
116119
let serviceAccount: ServiceAccountCredentials;
@@ -164,10 +167,13 @@ export async function initializeGoogleCloudCredentials({
164167
client_email: serviceAccount.client_email,
165168
private_key: serviceAccount.private_key,
166169
},
170+
...(googleCloudTranslationEndpoint && {
171+
apiEndpoint: googleCloudTranslationEndpoint,
172+
}),
167173
});
168174

169175
log.info(
170-
`[Google Cloud Auth] Initialized Translation client for project: ${serviceAccount.project_id}`,
176+
`[Google Cloud Auth] Initialized Translation client for project: ${serviceAccount.project_id}${googleCloudTranslationEndpoint ? ` with endpoint: ${googleCloudTranslationEndpoint}` : ""}`,
171177
);
172178

173179
return {

0 commit comments

Comments
 (0)