Skip to content
Closed
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
12 changes: 0 additions & 12 deletions apps/mobile/src/i18n/i18next.d.ts

This file was deleted.

127 changes: 127 additions & 0 deletions apps/mobile/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,131 @@
{
"providerReview": {
"title": "Review code",
"pullRequest": {
"title": "Pull request review",
"open": "Open pull request",
"review": "Review pull request",
"merge": "Merge pull request",
"confirmMerge": "Merge pull request?",
"paste": "Paste a pull request link",
"urlAccessibility": "Pull request URL",
"clearLink": "Clear pull request link",
"share": "Share pull request",
"badge": "Pull request #{{number}}",
"loading": "Loading pull request",
"unavailable": "This pull request is unavailable. Check the link and your repository access.",
"noReviews": "No pull requests in this scope. Paste a link or select a repository.",
"noDiscussion": "No discussion on this pull request yet. You can add a comment."
},
"mergeRequest": {
"title": "Merge request review",
"open": "Open merge request",
"review": "Review merge request",
"merge": "Merge merge request",
"confirmMerge": "Merge merge request?",
"paste": "Paste a merge request link",
"urlAccessibility": "Merge request URL",
"clearLink": "Clear merge request link",
"share": "Share merge request",
"badge": "Merge request !{{number}}",
"loading": "Loading merge request",
"unavailable": "This merge request is unavailable. Check the link and your repository access.",
"noReviews": "No merge requests in this scope. Paste a link or select a repository.",
"noDiscussion": "No discussion on this merge request yet. You can add a comment."
},
"identity": {
"owner": "Kilo owner: {{owner}}",
"actor": "{{provider}} actor: {{actor}}",
"integration": "Connection: {{integration}}",
"instance": "Instance: {{instance}}",
"repository": "Repository: {{repository}}",
"revision": "Revision: {{revision}}"
},
"connection": {
"connect": "Connect {{provider}}",
"reconnect": "Reconnect {{provider}}",
"expired": "Your {{provider}} connection expired. Reconnect to continue with your saved work.",
"personalBitbucket": "Bitbucket review is available in an organization, not Personal.",
"switchOrganization": "Switch organization",
"loading": "Loading {{provider}}"
},
"entry": {
"paste": "Paste a pull request or merge request link",
"invalidLink": "This link does not identify a supported review. Correct the link and try again.",
"ambiguous": "More than one connection matches this review. Select the exact connection.",
"noRecents": "No recent reviews. Paste a link to start.",
"openProvider": "View on {{provider}}"
},
"inbox": {
"actorScope": "Reviews for {{actor}} on {{provider}}",
"repositoryScope": "Reviews in {{repository}} using {{actor}}",
"pageFailed": "Could not load the next page. Your loaded reviews are still available.",
"retryPage": "Retry this page"
},
"files": {
"empty": "No changed files",
"binary": "This file is binary. View it on {{provider}}.",
"truncated": "This diff is incomplete. View the full diff on {{provider}}.",
"unavailable": "Context is unavailable at this revision. View the file on {{provider}}.",
"contextFailed": "Could not load context. Your file and line selection are unchanged.",
"position": "{{path}}, {{side}} side, line {{line}}, revision {{revision}}"
},
"checks": {
"empty": "No checks reported. This does not mean the checks passed.",
"unavailable": "Check results are unavailable for this connection.",
"openProvider": "View checks on {{provider}}"
},
"permission": {
"forbidden": "You do not have permission to perform this action. {{reason}}",
"unsupported": "This provider does not support this action. {{reason}}",
"version": "This action is unavailable on this provider version. {{reason}}",
"license": "This action requires a different provider license. {{reason}}",
"restricted": "A current repository restriction blocks this action. {{reason}}",
"unknown": "The connection has not confirmed whether this action is available.",
"readOnly": "You can read this review. This connection needs additional permission to write.",
"replaceToken": "Replace the connection token"
},
"draft": {
"stale": "The revision changed. Your text and original position are saved. Refresh before choosing a new position.",
"quarantined": "This saved draft has an unresolved account or repository identity. It has not been attached to this review.",
"emptyQueue": "No comments queued. Add a comment or a review summary.",
"saved": "Your review summary and choice are saved for this revision.",
"refresh": "Refresh review"
},
"outcome": {
"confirmed": "{{provider}} confirmed this action.",
"accepted": "{{provider}} accepted this action. Completion is not confirmed yet.",
"partial": "Confirmed: {{confirmed}}. Unfinished: {{unfinished}}. Your remaining work is saved.",
"unresolved": "The outcome is unknown. Check the status before sending anything again.",
"retryable": "The action did not start. Your work is saved. You can retry the same action.",
"rejected": "The action was rejected. Your work is saved. {{reason}}",
"checkStatus": "Check action status",
"storageFailed": "Could not save this action safely. Nothing was sent. Your text is still here."
},
"merge": {
"noMethods": "No merge method is available. {{reason}}",
"confirmDelete": "{{actor}} will merge {{source}} into {{target}} using {{method}} and delete the source branch {{source}}.",
"confirmKeep": "{{actor}} will merge {{source}} into {{target}} using {{method}} and keep the source branch {{source}}.",
"deleteBranch": "Delete source branch {{branch}} after merging",
"pendingTask": "The merge task is still running. You can leave and check its status later.",
"deletionFailed": "The merge is confirmed, but branch deletion failed. {{reason}}",
"deletionUnknown": "The merge is confirmed. Branch deletion is not confirmed.",
"changedHead": "The source or target revision changed. Refresh the review before merging."
},
"gitlab": {
"approvals": "Approvals needed: {{displayCount}}",
"changesRequested": "Changes requested",
"changesDoNotBlock": "Requested changes are recorded, but this instance does not enforce them as a merge block.",
"squashPolicy": "Squash policy: {{policy}}"
},
"bitbucket": {
"noExpectedHeadGuard": "Bitbucket cannot atomically bind this action to the reviewed revision. Head checks detect changes but cannot prevent a concurrent update.",
"participantState": "{{actor}}: {{state}}",
"noAutoMerge": "Bitbucket does not provide an API to schedule or cancel automatic merging.",
"noBranchSync": "Bitbucket does not provide an API to synchronize this branch.",
"noReactions": "Bitbucket does not provide comment reactions through this API."
}
},
"common": {
"cancel": "Cancel",
"done": "Done",
Expand Down
220 changes: 220 additions & 0 deletions apps/mobile/src/i18n/provider-review-vocabulary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { CODE_REVIEW_PLATFORMS } from '@kilocode/app-shared/code-review';
import { createInstance } from 'i18next';
import { type UseTranslationResponse } from 'react-i18next';
import { describe, expect, expectTypeOf, it } from 'vitest';

import { REPO_PLATFORM_LABEL_KEYS } from '@/lib/picker-bridge';

import en from './locales/en.json';
import {
PROVIDER_REVIEW_REQUEST_KEYS,
PROVIDER_REVIEW_STATE_KEYS,
type ProviderReviewKey,
} from './provider-review-vocabulary';

const i18n = createInstance();
await i18n.init({
resources: { en: { translation: en } },
lng: 'en',
fallbackLng: 'en',
interpolation: { escapeValue: false },
initAsync: false,
returnNull: false,
});

const pullRequestCopy = {
title: 'Pull request review',
open: 'Open pull request',
review: 'Review pull request',
merge: 'Merge pull request',
confirmMerge: 'Merge pull request?',
paste: 'Paste a pull request link',
urlAccessibility: 'Pull request URL',
clearLink: 'Clear pull request link',
share: 'Share pull request',
badge: 'Pull request #42',
loading: 'Loading pull request',
unavailable: 'This pull request is unavailable. Check the link and your repository access.',
noReviews: 'No pull requests in this scope. Paste a link or select a repository.',
noDiscussion: 'No discussion on this pull request yet. You can add a comment.',
};
const mergeRequestCopy = {
title: 'Merge request review',
open: 'Open merge request',
review: 'Review merge request',
merge: 'Merge merge request',
confirmMerge: 'Merge merge request?',
paste: 'Paste a merge request link',
urlAccessibility: 'Merge request URL',
clearLink: 'Clear merge request link',
share: 'Share merge request',
badge: 'Merge request !42',
loading: 'Loading merge request',
unavailable: 'This merge request is unavailable. Check the link and your repository access.',
noReviews: 'No merge requests in this scope. Paste a link or select a repository.',
noDiscussion: 'No discussion on this merge request yet. You can add a comment.',
};
const expectedRequestCopy = {
github: pullRequestCopy,
gitlab: mergeRequestCopy,
bitbucket: pullRequestCopy,
};

type StringTree = { [key: string]: string | StringTree };

function leafEntries(tree: StringTree, prefix = ''): [string, string][] {
return Object.entries(tree).flatMap(([name, value]) => {
const path = prefix ? `${prefix}.${name}` : name;
return typeof value === 'string' ? [[path, value]] : leafEntries(value, path);
});
}

describe('provider review vocabulary', () => {
it('keeps vocabulary inputs constrained to catalog leaves', () => {
const key = PROVIDER_REVIEW_REQUEST_KEYS.gitlab.title satisfies ProviderReviewKey;

expectTypeOf<ProviderReviewKey>().not.toBeNever();
expectTypeOf<'providerReview.title'>().toExtend<ProviderReviewKey>();
expectTypeOf<'providerReview.permission.forbidden'>().toExtend<ProviderReviewKey>();
expectTypeOf<'providerReview.mergeRequest.unknown'>().not.toExtend<ProviderReviewKey>();
expectTypeOf<'providerReview'>().not.toExtend<ProviderReviewKey>();
expectTypeOf<'providerReview.mergeRequest'>().not.toExtend<ProviderReviewKey>();
expectTypeOf<'providerReview.permission'>().not.toExtend<ProviderReviewKey>();
expectTypeOf<'common.cancel'>().not.toExtend<ProviderReviewKey>();
expectTypeOf<`providerReview.${string}`>().not.toExtend<ProviderReviewKey>();
expectTypeOf<string>().not.toExtend<ProviderReviewKey>();
expect(i18n.t(key)).toBe('Merge request review');
});

it('keeps legacy string inputs usable with the instance translator', () => {
const translate = (key: string) => i18n.t(key);
const label = translate('common.cancel');

expectTypeOf(label).toEqualTypeOf<string>();
expect(label).toBe('Cancel');
});

it('keeps widened repository labels usable with the hook translator', () => {
const translate = (t: UseTranslationResponse<'translation', undefined>['t']) =>
t(REPO_PLATFORM_LABEL_KEYS.gitlab);
const label = translate(i18n.t);

expectTypeOf(label).toEqualTypeOf<string>();
expect(`${label}: example/repo`).toBe('GitLab: example/repo');
});

it.each(CODE_REVIEW_PLATFORMS)('renders the request terminology for %s', provider => {
const rendered = Object.fromEntries(
Object.entries(PROVIDER_REVIEW_REQUEST_KEYS[provider]).map(([field, key]) => [
field,
i18n.t(key, { number: 42 }),
])
);

expect(rendered).toEqual(expectedRequestCopy[provider]);
});

it('resolves exactly the English providerReview leaves without missing or extra keys', () => {
const keys = new Set(
[
...leafEntries(PROVIDER_REVIEW_REQUEST_KEYS),
...leafEntries(PROVIDER_REVIEW_STATE_KEYS),
].map(([, key]) => key)
);
const resolved = Object.fromEntries(
[...keys].map(key => [key, i18n.getResource('en', 'translation', key)])
);

expect(resolved).toStrictEqual(
Object.fromEntries(leafEntries(en.providerReview, 'providerReview'))
);
});

it('keeps permission failures distinct from provider limitations and recovery labels', () => {
const rendered = Object.fromEntries(
Object.entries(PROVIDER_REVIEW_STATE_KEYS.permission).map(([state, key]) => [
state,
i18n.t(key, { reason: 'Repository policy.' }),
])
);

expect(rendered).toEqual({
forbidden: 'You do not have permission to perform this action. Repository policy.',
unsupported: 'This provider does not support this action. Repository policy.',
version: 'This action is unavailable on this provider version. Repository policy.',
license: 'This action requires a different provider license. Repository policy.',
restricted: 'A current repository restriction blocks this action. Repository policy.',
unknown: 'The connection has not confirmed whether this action is available.',
readOnly: 'You can read this review. This connection needs additional permission to write.',
replaceToken: 'Replace the connection token',
});
});

it('preserves stale draft positions and keeps unresolved identities quarantined', () => {
const { draft } = PROVIDER_REVIEW_STATE_KEYS;

expect(i18n.t(draft.stale)).toBe(
'The revision changed. Your text and original position are saved. Refresh before choosing a new position.'
);
expect(i18n.t(draft.quarantined)).toBe(
'This saved draft has an unresolved account or repository identity. It has not been attached to this review.'
);
expect(i18n.t(draft.refresh)).toBe('Refresh review');
});

it('distinguishes confirmed, pending, partial, retryable, and rejected outcomes', () => {
const rendered = Object.fromEntries(
Object.entries(PROVIDER_REVIEW_STATE_KEYS.outcome).map(([state, key]) => [
state,
i18n.t(key, {
provider: 'Bitbucket',
confirmed: 2,
unfinished: 1,
reason: 'Access denied.',
}),
])
);

expect(rendered).toEqual({
confirmed: 'Bitbucket confirmed this action.',
accepted: 'Bitbucket accepted this action. Completion is not confirmed yet.',
partial: 'Confirmed: 2. Unfinished: 1. Your remaining work is saved.',
unresolved: 'The outcome is unknown. Check the status before sending anything again.',
retryable: 'The action did not start. Your work is saved. You can retry the same action.',
rejected: 'The action was rejected. Your work is saved. Access denied.',
checkStatus: 'Check action status',
storageFailed:
'Could not save this action safely. Nothing was sent. Your text is still here.',
});
});

it('does not present missing checks or an empty comment queue as completed work', () => {
expect(i18n.t(PROVIDER_REVIEW_STATE_KEYS.checks.empty)).toBe(
'No checks reported. This does not mean the checks passed.'
);
expect(i18n.t(PROVIDER_REVIEW_STATE_KEYS.draft.emptyQueue)).toBe(
'No comments queued. Add a comment or a review summary.'
);
});

it('preserves provider limits and separates merge completion from branch deletion', () => {
const { bitbucket, gitlab, merge } = PROVIDER_REVIEW_STATE_KEYS;

expect(i18n.t(gitlab.approvals, { displayCount: '2' })).toBe('Approvals needed: 2');
expect(i18n.t(gitlab.changesDoNotBlock)).toBe(
'Requested changes are recorded, but this instance does not enforce them as a merge block.'
);
expect(i18n.t(bitbucket.noExpectedHeadGuard)).toBe(
'Bitbucket cannot atomically bind this action to the reviewed revision. Head checks detect changes but cannot prevent a concurrent update.'
);
expect(i18n.t(merge.pendingTask)).toBe(
'The merge task is still running. You can leave and check its status later.'
);
expect(i18n.t(merge.deletionFailed, { reason: 'Protected branch.' })).toBe(
'The merge is confirmed, but branch deletion failed. Protected branch.'
);
expect(i18n.t(merge.deletionUnknown)).toBe(
'The merge is confirmed. Branch deletion is not confirmed.'
);
});
});
Loading
Loading