Skip to content

Commit 71dd293

Browse files
Jing-yilinclaude
andauthored
feat: Add Vertex AI Chinese translation support (#42)
* feat: add Chinese translation via Vertex AI Implement complete Chinese localization for KickWatch: **Database Schema:** - Add `name_zh`, `blurb_zh`, `creator_name_zh` to Campaign model - Add `name_zh` to Category model **Vertex AI Translation Service:** - New TranslatorService using Vertex AI Gemini 2.0 Flash - Batch translation (10 campaigns per API call) to optimize costs - Uses GCP startup credits from rescience-lab-465304 project - Automatic translation during nightly crawl and backfill **Category Localization:** - All 14 root categories with Chinese names - 12 subcategories with Chinese names **Configuration:** - Add VERTEX_AI_PROJECT_ID and VERTEX_AI_LOCATION env vars - Service account credentials via GOOGLE_SERVICE_ACCOUNT_JSON - AWS Secrets Manager integration ready **Infrastructure:** - GCP Project: rescience-lab-465304 - Service Account: kickwatch-translator@rescience-lab-465304.iam.gserviceaccount.com - AWS Secrets: kickwatch-dev/google-service-account, kickwatch-dev/vertex-ai-config **Technical Details:** - Translator gracefully degrades if not configured (no blocking) - Continues crawl even if translation fails - Temperature=0.3 for consistent translations - 500ms delay between batches to avoid rate limits Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(ios): add Chinese content display support Update iOS app to display translated Chinese content: **CampaignDTO & CategoryDTO:** - Add `name_zh`, `blurb_zh`, `creator_name_zh` fields - Add computed properties `displayName`, `displayBlurb`, `displayCreatorName` - Automatically fallback to English if Chinese translation unavailable **Views Updated:** - CampaignRowView: Use displayName/displayCreatorName - CampaignDetailView: Use displayName/displayBlurb/displayCreatorName - DiscoverView: Use displayName for categories - WatchlistView: Updated DTO initialization **User Experience:** - Chinese content shows first when available - Seamless fallback to English - No code changes needed when backend adds translations Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * ci: add Vertex AI environment variables to ECS deployment Add Vertex AI configuration to GitHub Actions workflow: - VERTEX_AI_PROJECT_ID=rescience-lab-465304 - VERTEX_AI_LOCATION=us-central1 - GOOGLE_SERVICE_ACCOUNT_JSON from AWS Secrets Manager This enables automatic Chinese translation during nightly crawls. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: resolve Codex review findings for Chinese translation Fixes 3 issues identified in PR #42 code review: 1. MockAPIClient.swift: Add missing Chinese fields (name_zh, blurb_zh, creator_name_zh) to CampaignDTO test factory 2. APIClient.swift: Handle empty string fallback in computed properties. Backend emits empty strings for untranslated fields, so nil-coalescing alone is insufficient. Check for both nil AND empty string. 3. cron.go: Include name_zh in syncCategories() upsert columns so existing category rows get Chinese names on deployment Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 2d02ed5 commit 71dd293

15 files changed

Lines changed: 416 additions & 58 deletions

File tree

.github/workflows/deploy-backend.yml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ jobs:
117117
echo "apns_bundle_id_arn=$(get_arn ${SECRET_PREFIX}/apns-bundle-id)" >> $GITHUB_OUTPUT
118118
echo "apns_key_arn=$(get_arn ${SECRET_PREFIX}/apns-key)" >> $GITHUB_OUTPUT
119119
echo "scrapingbee_api_key_arn=$(get_arn ${SECRET_PREFIX}/scrapingbee-api-key)" >> $GITHUB_OUTPUT
120+
echo "google_sa_arn=$(get_arn ${SECRET_PREFIX}/google-service-account)" >> $GITHUB_OUTPUT
120121
121122
- name: Generate ECS task definition
122123
env:
@@ -140,18 +141,21 @@ jobs:
140141
{ "containerPort": 8080, "protocol": "tcp" }
141142
],
142143
"environment": [
143-
{ "name": "PORT", "value": "8080" },
144-
{ "name": "GIN_MODE", "value": "${{ env.GIN_MODE }}" },
145-
{ "name": "APP_ENV", "value": "${{ env.DEPLOY_ENV }}" },
146-
{ "name": "APNS_ENV", "value": "${{ env.IS_PROD == 'true' && 'production' || 'sandbox' }}" }
144+
{ "name": "PORT", "value": "8080" },
145+
{ "name": "GIN_MODE", "value": "${{ env.GIN_MODE }}" },
146+
{ "name": "APP_ENV", "value": "${{ env.DEPLOY_ENV }}" },
147+
{ "name": "APNS_ENV", "value": "${{ env.IS_PROD == 'true' && 'production' || 'sandbox' }}" },
148+
{ "name": "VERTEX_AI_PROJECT_ID", "value": "rescience-lab-465304" },
149+
{ "name": "VERTEX_AI_LOCATION", "value": "us-central1" }
147150
],
148151
"secrets": [
149-
{ "name": "DATABASE_URL", "valueFrom": "${{ steps.secrets.outputs.db_arn }}" },
150-
{ "name": "APNS_KEY_ID", "valueFrom": "${{ steps.secrets.outputs.apns_key_id_arn }}" },
151-
{ "name": "APNS_TEAM_ID", "valueFrom": "${{ steps.secrets.outputs.apns_team_id_arn }}" },
152-
{ "name": "APNS_BUNDLE_ID", "valueFrom": "${{ steps.secrets.outputs.apns_bundle_id_arn }}" },
153-
{ "name": "APNS_KEY", "valueFrom": "${{ steps.secrets.outputs.apns_key_arn }}" },
154-
{ "name": "SCRAPINGBEE_API_KEY", "valueFrom": "${{ steps.secrets.outputs.scrapingbee_api_key_arn }}" }
152+
{ "name": "DATABASE_URL", "valueFrom": "${{ steps.secrets.outputs.db_arn }}" },
153+
{ "name": "APNS_KEY_ID", "valueFrom": "${{ steps.secrets.outputs.apns_key_id_arn }}" },
154+
{ "name": "APNS_TEAM_ID", "valueFrom": "${{ steps.secrets.outputs.apns_team_id_arn }}" },
155+
{ "name": "APNS_BUNDLE_ID", "valueFrom": "${{ steps.secrets.outputs.apns_bundle_id_arn }}" },
156+
{ "name": "APNS_KEY", "valueFrom": "${{ steps.secrets.outputs.apns_key_arn }}" },
157+
{ "name": "SCRAPINGBEE_API_KEY", "valueFrom": "${{ steps.secrets.outputs.scrapingbee_api_key_arn }}" },
158+
{ "name": "GOOGLE_SERVICE_ACCOUNT_JSON", "valueFrom": "${{ steps.secrets.outputs.google_sa_arn }}" }
155159
],
156160
"readonlyRootFilesystem": true,
157161
"linuxParameters": { "initProcessEnabled": true },

backend/cmd/api/main.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"log"
56
"time"
67

@@ -48,7 +49,22 @@ func main() {
4849
log.Printf("APNs init failed (push disabled): %v", err)
4950
}
5051
}
51-
cronSvc = service.NewCronService(db.DB, scrapingService, apnsClient)
52+
53+
// Initialize Vertex AI translator
54+
var translator *service.TranslatorService
55+
if cfg.VertexAIProjectID != "" && cfg.VertexAILocation != "" {
56+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
57+
defer cancel()
58+
var err error
59+
translator, err = service.NewTranslatorService(ctx, cfg.VertexAIProjectID, cfg.VertexAILocation)
60+
if err != nil {
61+
log.Printf("Vertex AI translator init failed (translation disabled): %v", err)
62+
} else {
63+
log.Printf("Vertex AI translator initialized (project=%s, location=%s)", cfg.VertexAIProjectID, cfg.VertexAILocation)
64+
}
65+
}
66+
67+
cronSvc = service.NewCronService(db.DB, scrapingService, apnsClient, translator)
5268
cronSvc.Start()
5369
defer cronSvc.Stop()
5470

backend/go.mod

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,35 @@ module github.com/kickwatch/backend
33
go 1.25.5
44

55
require (
6+
cloud.google.com/go v0.121.2 // indirect
7+
cloud.google.com/go/aiplatform v1.90.0 // indirect
8+
cloud.google.com/go/auth v0.16.2 // indirect
9+
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
10+
cloud.google.com/go/compute/metadata v0.7.0 // indirect
11+
cloud.google.com/go/iam v1.5.2 // indirect
12+
cloud.google.com/go/longrunning v0.6.7 // indirect
13+
cloud.google.com/go/vertexai v0.15.0 // indirect
614
github.com/PuerkitoBio/goquery v1.11.0 // indirect
715
github.com/andybalholm/cascadia v1.3.3 // indirect
816
github.com/bytedance/sonic v1.14.0 // indirect
917
github.com/bytedance/sonic/loader v0.3.0 // indirect
1018
github.com/cloudwego/base64x v0.1.6 // indirect
19+
github.com/felixge/httpsnoop v1.0.4 // indirect
1120
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
1221
github.com/gin-contrib/sse v1.1.0 // indirect
1322
github.com/gin-gonic/gin v1.11.0 // indirect
23+
github.com/go-logr/logr v1.4.2 // indirect
24+
github.com/go-logr/stdr v1.2.2 // indirect
1425
github.com/go-playground/locales v0.14.1 // indirect
1526
github.com/go-playground/universal-translator v0.18.1 // indirect
1627
github.com/go-playground/validator/v10 v10.27.0 // indirect
1728
github.com/goccy/go-json v0.10.2 // indirect
1829
github.com/goccy/go-yaml v1.18.0 // indirect
1930
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
31+
github.com/google/s2a-go v0.1.9 // indirect
2032
github.com/google/uuid v1.6.0 // indirect
33+
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
34+
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
2135
github.com/jackc/pgpassfile v1.0.0 // indirect
2236
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
2337
github.com/jackc/pgx/v5 v5.6.0 // indirect
@@ -37,15 +51,28 @@ require (
3751
github.com/robfig/cron/v3 v3.0.1 // indirect
3852
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
3953
github.com/ugorji/go/codec v1.3.0 // indirect
54+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
55+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
56+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
57+
go.opentelemetry.io/otel v1.36.0 // indirect
58+
go.opentelemetry.io/otel/metric v1.36.0 // indirect
59+
go.opentelemetry.io/otel/trace v1.36.0 // indirect
4060
go.uber.org/mock v0.5.0 // indirect
4161
golang.org/x/arch v0.20.0 // indirect
4262
golang.org/x/crypto v0.44.0 // indirect
4363
golang.org/x/mod v0.29.0 // indirect
4464
golang.org/x/net v0.47.0 // indirect
65+
golang.org/x/oauth2 v0.30.0 // indirect
4566
golang.org/x/sync v0.18.0 // indirect
4667
golang.org/x/sys v0.38.0 // indirect
4768
golang.org/x/text v0.31.0 // indirect
69+
golang.org/x/time v0.12.0 // indirect
4870
golang.org/x/tools v0.38.0 // indirect
71+
google.golang.org/api v0.237.0 // indirect
72+
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
73+
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
74+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
75+
google.golang.org/grpc v1.73.0 // indirect
4976
google.golang.org/protobuf v1.36.9 // indirect
5077
gorm.io/driver/postgres v1.6.0 // indirect
5178
gorm.io/gorm v1.31.1 // indirect

backend/go.sum

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg=
2+
cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
3+
cloud.google.com/go/aiplatform v1.90.0 h1:QdNBP8/2HtWYMXZczGd5LsL72lTiMyzliXgBSk7R9HE=
4+
cloud.google.com/go/aiplatform v1.90.0/go.mod h1:ouoFeopVQaYTFwvviZJi17excXiwMGi+HvznNH2B1tw=
5+
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
6+
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
7+
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
8+
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
9+
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
10+
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
11+
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
12+
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
13+
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
14+
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
15+
cloud.google.com/go/vertexai v0.15.0 h1:FRVdUsm07qX9P/19SMDd/RZVwLR9sCm3HN0Ze7wSEpc=
16+
cloud.google.com/go/vertexai v0.15.0/go.mod h1:YTy1fUT3yH57nClxotpyY29T0MhnNUHIyysef8u69ow=
117
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
218
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
319
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@@ -10,12 +26,19 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI
1026
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
1127
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1228
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
29+
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
30+
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
1331
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
1432
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
1533
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
1634
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
1735
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
1836
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
37+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
38+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
39+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
40+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
41+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
1942
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
2043
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
2144
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -30,8 +53,14 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y
3053
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
3154
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3255
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
56+
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
57+
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
3358
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3459
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
60+
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
61+
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
62+
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
63+
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
3564
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
3665
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
3766
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -80,6 +109,18 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
80109
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
81110
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
82111
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
112+
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
113+
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
114+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
115+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
116+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
117+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
118+
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
119+
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
120+
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
121+
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
122+
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
123+
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
83124
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
84125
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
85126
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
@@ -116,6 +157,8 @@ golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
116157
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
117158
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
118159
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
160+
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
161+
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
119162
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
120163
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
121164
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -165,6 +208,8 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
165208
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
166209
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
167210
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
211+
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
212+
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
168213
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
169214
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
170215
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -176,6 +221,16 @@ golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg
176221
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
177222
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
178223
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
224+
google.golang.org/api v0.237.0 h1:MP7XVsGZesOsx3Q8WVa4sUdbrsTvDSOERd3Vh4xj/wc=
225+
google.golang.org/api v0.237.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
226+
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
227+
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
228+
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
229+
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
230+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
231+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
232+
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
233+
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
179234
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
180235
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
181236
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

backend/internal/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ type Config struct {
1717
// ScrapingBee configuration
1818
ScrapingBeeAPIKey string
1919
ScrapingBeeMaxConcurrent int
20+
// Vertex AI configuration
21+
VertexAIProjectID string
22+
VertexAILocation string
2023
}
2124

2225
func Load() *Config {
@@ -47,5 +50,7 @@ func Load() *Config {
4750
APNSEnv: apnsEnv,
4851
ScrapingBeeAPIKey: os.Getenv("SCRAPINGBEE_API_KEY"),
4952
ScrapingBeeMaxConcurrent: maxConcurrent,
53+
VertexAIProjectID: os.Getenv("VERTEX_AI_PROJECT_ID"),
54+
VertexAILocation: os.Getenv("VERTEX_AI_LOCATION"),
5055
}
5156
}

backend/internal/model/model.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
type Campaign struct {
1111
PID string `gorm:"column:pid;primaryKey" json:"pid"`
1212
Name string `gorm:"not null" json:"name"`
13+
NameZh string `json:"name_zh"`
1314
Blurb string `json:"blurb"`
15+
BlurbZh string `json:"blurb_zh"`
1416
PhotoURL string `json:"photo_url"`
1517
GoalAmount float64 `json:"goal_amount"`
1618
GoalCurrency string `json:"goal_currency"`
@@ -21,6 +23,7 @@ type Campaign struct {
2123
CategoryName string `json:"category_name"`
2224
ProjectURL string `json:"project_url"`
2325
CreatorName string `json:"creator_name"`
26+
CreatorNameZh string `json:"creator_name_zh"`
2427
PercentFunded float64 `json:"percent_funded"`
2528
BackersCount int `gorm:"default:0" json:"backers_count"`
2629
Slug string `json:"slug"`
@@ -51,6 +54,7 @@ func (s *CampaignSnapshot) BeforeCreate(tx *gorm.DB) error {
5154
type Category struct {
5255
ID string `gorm:"primaryKey" json:"id"`
5356
Name string `gorm:"not null" json:"name"`
57+
NameZh string `json:"name_zh"`
5458
ParentID string `json:"parent_id,omitempty"`
5559
}
5660

0 commit comments

Comments
 (0)