This file provides Tolgee-specific guidance for AI coding agents working on the Tolgee localization platform.
This project uses a multi-repository setup managed by a wrapper repository:
tolgee-platform/ # platform-dev-start (wrapper repo)
├── .git/ # git@github.com:tolgee/platform-dev-start.git
├── start.sh # Development startup script
├── public/ # tolgee-platform (main repo)
│ ├── .git/ # git@github.com:tolgee/tolgee-platform.git
│ ├── backend/ # Kotlin/Spring Boot backend
│ ├── webapp/ # React frontend
│ └── e2e/ # Cypress E2E tests
└── billing/ # billing (private repo)
└── .git/ # git@github.com:tolgee/billing.git
Important: When working with git commands, ensure you're in the correct repository:
- For platform code changes:
cd public/then run git commands - For billing changes:
cd billing/then run git commands - The wrapper repo (
platform-dev-start) ignorespublic/andbilling/in its.gitignore
After modifying JPA entities, always run:
./gradlew diffChangeLogThis generates Liquibase changelog entries. If you get "docker command not found", add --no-daemon flag.
Tests are split into multiple categories that run in parallel in CI:
./gradlew server-app:runContextRecreatingTests && \
./gradlew server-app:runStandardTests && \
./gradlew server-app:runWebsocketTests && \
./gradlew server-app:runWithoutEeTests && \
./gradlew ee-test:test && \
./gradlew data:test && \
./gradlew security:testDon't use the bare test task (it doesn't work) – always run a specific test suite even when running a single test, e.g:
# Don't do this
./gradlew test --tests "io.tolgee.unit.formats.android.out.AndroidSdkFileExporterTest"
# Do this
./gradlew :data:test --tests "io.tolgee.unit.formats.android.out.AndroidSdkFileExporterTest"To see test output in real-time (like in an IDE), use --console=plain with grep to filter relevant logs:
# Run a specific test with visible INFO logs
./gradlew :server-app:test --tests "io.tolgee.batch.SomeTest" --console=plain --info 2>&1 | grep -E "(INFO.*SomeTest|ERROR|WARN)" | head -100
# See all test output without filtering
./gradlew :server-app:test --tests "io.tolgee.batch.SomeTest" --console=plain --info 2>&1 | tail -200Use logger.info() in tests for diagnostic output that will be visible with these commands.
TestData Pattern: Use TestData classes for test setup:
class YourControllerTest {
@Autowired
lateinit var testDataService: TestDataService
lateinit var testData: YourTestData
@BeforeEach
fun setup() {
testData = YourTestData()
testDataService.saveTestData(testData.root)
userAccount = testData.user
}
@AfterEach
fun cleanup() {
testDataService.cleanTestData(testData.root)
}
}JSON Response Testing: Use .andAssertThatJson for API responses:
performProjectAuthGet("items").andAssertThatJson {
node("_embedded.items") {
node("[0].id").isEqualTo(1)
node("[0].name").isEqualTo("Item name")
}
node("page.totalElements").isNumber.isEqualTo(BigDecimal(2))
}Always run before commits:
./gradlew ktlintFormatTolgee uses custom TypeScript path aliases instead of relative imports:
tg.component/*→component/*tg.service/*→service/*tg.hooks/*→hooks/*tg.views/*→views/*tg.globalContext/*→globalContext/*
Example: import { useUser } from 'tg.hooks/useUser'
After backend API changes, regenerate TypeScript types. Backend must be running first:
# 1. Start backend (in separate terminal)
./gradlew server-app:bootRun --args='--spring.profiles.active=dev'
# 2. Regenerate schemas
cd webapp
npm run schema # For main API
npm run billing-schema # For billing API (if applicable)Use typed React Query hooks from useQueryApi.ts (not raw React Query):
// Query example
const { data, isLoading } = useApiQuery({
url: '/v2/projects/{projectId}/languages',
method: 'get',
path: { projectId: project.id },
});
// Mutation example
const mutation = useApiMutation({
url: '/v2/projects/{projectId}/languages',
method: 'post',
invalidatePrefix: '/v2/projects',
});
const handleSubmit = (data) => {
mutation.mutate({
path: { projectId: project.id },
content: data,
});
};Use Tolgee-specific hooks for analytics:
import { useReportEvent } from 'tg.hooks/useReportEvent';
const reportEvent = useReportEvent();
reportEvent('event_name', { key: 'value' });
// For component mount events:
import { useReportOnce } from 'tg.hooks/useReportEvent';
useReportOnce('page_viewed', { pageName: 'settings' });Creating E2E test data requires 3 components:
- TestData Class (
backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/YourFeatureTestData.kt):
class YourTestData : BaseTestData() {
val specificEntity: Entity
init {
root.apply {
specificEntity = addEntity {
name = "Test Entity"
}.self
}
}
}- E2E Data Controller (
backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/YourFeatureE2eDataController.kt):
@RestController
@RequestMapping("/api/internal/e2e-data/your-feature")
class YourFeatureE2eDataController : AbstractE2eDataController() {
// Implement data generation endpoints
}- Frontend Test Data Object (
e2e/cypress/common/apiCalls/testData/testData.ts):
export const yourFeatureTestData = generateTestDataObject('your-feature');Usage in tests:
beforeEach(() => {
yourFeatureTestData.clean();
yourFeatureTestData.generateStandard().then((r) => {
const testData = r.body;
// Use testData in your tests
});
});Note: Use generateStandard(), not generate() (outdated pattern).
STRICTLY ENFORCED: Always use data-cy attributes for selectors, never text content.
- All data-cy values are typed in
e2e/cypress/support/dataCyType.d.ts(auto-generated, don't modify) - Use typed helpers:
gcy('...')orcy.gcy('...') - Add data-cy to all components accessed from tests
- Make data-cy attributes specific and descriptive
Example:
// Component
<Alert severity="error" data-cy="signup-error-seats-spending-limit">
<T keyName="spending_limit_dialog_title" />
</Alert>
// Test (GOOD)
gcy('signup-error-seats-spending-limit').should('be.visible');
// Test (BAD - don't use text content)
cy.contains('exceeded').should('be.visible');Backend error codes use Message.kt enum, converted to lowercase when sent to frontend:
cy.intercept('POST', '/v2/projects/*/keys*', {
statusCode: 400,
body: {
code: 'plan_key_limit_exceeded', // lowercase
params: [1000, 1001],
},
}).as('createKey');Format: firstname-lastname/feature-description
Generate name from git config:
git config get user.name | awk '{print $1, $2}' | \
iconv -f UTF-8 -t ASCII//TRANSLIT | \
tr -cd '[:alpha:]' | tr '[:upper:]' '[:lower:]'feat:- Breaking changes or new featuresfix:- Non-breaking bug fixeschore:- Non-behavior changes (docs, tests, formatting)
Example: feat: add CSV export feature
NEVER update translation files with new keys manually. Translation keys are automatically added to files after your changes are merged to the main branch. Freely use nonexistent keys in code - they'll be handled outside the codebase.