Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
1 change: 1 addition & 0 deletions crates/services/src/services/config/versions/v6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum UiLanguage {
Ko, // Force Korean
ZhHans, // Force Simplified Chinese
ZhHant, // Force Traditional Chinese
He, // Force Hebrew
}

#[derive(Clone, Debug, Serialize, Deserialize, TS)]
Expand Down
66 changes: 40 additions & 26 deletions packages/web-core/src/features/onboarding/ui/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@phosphor-icons/react';
import type { IconProps } from '@phosphor-icons/react';
import { usePostHog } from 'posthog-js/react';
import { useTranslation } from 'react-i18next';
import { siDiscord } from 'simple-icons';
import {
BaseCodingAgent,
Expand All @@ -43,44 +44,44 @@ import { PrimaryButton } from '@vibe/ui/components/PrimaryButton';

type SoundOption = {
value: SoundFile;
label: string;
labelKey: string;
icon: Icon;
};

const SOUND_OPTIONS: SoundOption[] = [
{
value: SoundFile.ABSTRACT_SOUND1,
label: 'Abstract Sound 1',
labelKey: 'onboardingLanding.soundOptions.abstract1',
icon: WaveformIcon,
},
{
value: SoundFile.ABSTRACT_SOUND2,
label: 'Abstract Sound 2',
labelKey: 'onboardingLanding.soundOptions.abstract2',
icon: MusicNoteIcon,
},
{
value: SoundFile.ABSTRACT_SOUND3,
label: 'Abstract Sound 3',
labelKey: 'onboardingLanding.soundOptions.abstract3',
icon: MusicNotesIcon,
},
{
value: SoundFile.ABSTRACT_SOUND4,
label: 'Abstract Sound 4',
labelKey: 'onboardingLanding.soundOptions.abstract4',
icon: SpeakerHighIcon,
},
{
value: SoundFile.COW_MOOING,
label: 'Cow Mooing',
labelKey: 'onboardingLanding.soundOptions.cowMooing',
icon: CowIcon,
},
{
value: SoundFile.PHONE_VIBRATION,
label: 'Phone Vibration',
labelKey: 'onboardingLanding.soundOptions.phoneVibration',
icon: DeviceMobileIcon,
},
{
value: SoundFile.ROOSTER,
label: 'Rooster',
labelKey: 'onboardingLanding.soundOptions.rooster',
icon: BirdIcon,
},
];
Expand Down Expand Up @@ -147,6 +148,7 @@ function resolveTheme(theme: ThemeMode): 'light' | 'dark' {
}

export function LandingPage() {
const { t } = useTranslation('common');
const appNavigation = useAppNavigation();
const { theme } = useTheme();
Comment thread
lidorshimoni marked this conversation as resolved.
const { config, profiles, updateAndSaveConfig, loading } = useUserSystem();
Expand Down Expand Up @@ -316,7 +318,7 @@ export function LandingPage() {
if (loading || !config || !initialized) {
return (
<div className="h-screen bg-primary flex items-center justify-center">
<p className="text-low">Loading...</p>
<p className="text-low">{t('states.loading')}</p>
</div>
);
}
Expand Down Expand Up @@ -357,17 +359,17 @@ export function LandingPage() {
weight="fill"
/>
<p className="text-sm text-normal">
Vibe Kanban runs AI coding agents with{' '}
{t('onboardingLanding.safetyNotice.beforeFlags')}{' '}
<code>--dangerously-skip-permissions</code> /{' '}
<code>--yolo</code> by default. Always review what agents are
doing.{' '}
<code>--yolo</code>{' '}
{t('onboardingLanding.safetyNotice.afterFlags')}{' '}
<a
href="https://www.vibekanban.com/docs/getting-started#safety-notice"
target="_blank"
rel="noopener noreferrer"
className="text-brand hover:underline"
>
Learn more
{t('onboardingLanding.safetyNotice.learnMore')}
</a>
.
</p>
Expand All @@ -380,7 +382,9 @@ export function LandingPage() {
<div className="grid grid-cols-3 gap-double">
{/* Column 1: Coding Agent */}
<section className="space-y-half">
<h2 className="text-sm font-medium text-high">Coding Agent</h2>
<h2 className="text-sm font-medium text-high">
{t('onboardingLanding.codingAgent')}
</h2>
<div className="grid gap-1.5">
{executorOptions.map((agent) => {
const selected = selectedAgent === agent;
Expand Down Expand Up @@ -418,7 +422,9 @@ export function LandingPage() {

{/* Column 2: Code Editor */}
<section className="space-y-half">
<h2 className="text-sm font-medium text-high">Code Editor</h2>
<h2 className="text-sm font-medium text-high">
{t('onboardingLanding.codeEditor')}
</h2>
<div className="grid gap-1.5">
{editorOptions.map((editor) => {
const selected = editorType === editor;
Expand Down Expand Up @@ -456,13 +462,15 @@ export function LandingPage() {
{editorType === EditorType.CUSTOM && (
<div className="space-y-half">
<label className="text-sm font-medium text-normal">
Custom Command
{t('onboardingLanding.customCommand')}
</label>
<input
type="text"
value={customCommand}
onChange={(e) => setCustomCommand(e.target.value)}
placeholder="e.g. code --wait"
placeholder={t(
'onboardingLanding.customCommandPlaceholder'
)}
className={cn(
'w-full bg-panel border rounded-sm px-base py-half text-sm text-high',
'placeholder:text-low placeholder:opacity-80 focus:outline-none',
Expand All @@ -479,7 +487,7 @@ export function LandingPage() {
{/* Column 3: Notification Sound */}
<section className="space-y-half">
<h2 className="text-sm font-medium text-high">
Notification Sound
{t('onboardingLanding.notificationSound')}
</h2>
<div className="grid gap-1.5">
{SOUND_OPTIONS.map((option) => {
Expand All @@ -506,7 +514,7 @@ export function LandingPage() {
weight={selected ? 'fill' : 'bold'}
/>
<span className="text-sm text-normal flex-1 truncate">
{option.label}
{t(option.labelKey)}
</span>
{selected && (
<CheckIcon
Expand Down Expand Up @@ -534,7 +542,9 @@ export function LandingPage() {
)}
weight={!soundEnabled ? 'fill' : 'bold'}
/>
<span className="text-sm text-normal flex-1">No sound</span>
<span className="text-sm text-normal flex-1">
{t('onboardingLanding.noSound')}
</span>
{!soundEnabled && (
<CheckIcon
className="size-icon-xs text-brand shrink-0"
Expand All @@ -550,28 +560,32 @@ export function LandingPage() {
{/* Footer */}
<div className="shrink-0 border-t border-border p-double pt-base flex items-center justify-between gap-base">
<p className="text-xs text-low">
By continuing you agree to the{' '}
{t('onboardingLanding.footer.beforeTerms')}
<a
href="https://www.vibekanban.com/terms"
target="_blank"
rel="noopener noreferrer"
className="text-brand hover:underline"
>
terms and conditions
</a>{' '}
and{' '}
{t('onboardingLanding.footer.termsAndConditions')}
</a>
{t('onboardingLanding.footer.and')}
<a
href="https://www.vibekanban.com/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-brand hover:underline"
>
privacy policy
{t('onboardingLanding.footer.privacyPolicy')}
Comment thread
cursor[bot] marked this conversation as resolved.
</a>
.
</p>
<PrimaryButton
value={saving ? 'Saving...' : 'Continue'}
value={
saving
? t('onboardingLanding.actions.saving')
: t('onboardingLanding.actions.continue')
}
onClick={handleContinue}
disabled={!canContinue}
/>
Expand Down
33 changes: 29 additions & 4 deletions packages/web-core/src/i18n/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { SUPPORTED_I18N_CODES, uiLanguageToI18nCode } from './languages';
import {
SUPPORTED_I18N_CODES,
uiLanguageToI18nCode,
updateDocumentDirection,
} from './languages';

// Import translation files
import enCommon from './locales/en/common.json';
Expand Down Expand Up @@ -39,6 +43,11 @@ import zhHantSettings from './locales/zh-Hant/settings.json';
import zhHantProjects from './locales/zh-Hant/projects.json';
import zhHantTasks from './locales/zh-Hant/tasks.json';
import zhHantOrganization from './locales/zh-Hant/organization.json';
import heCommon from './locales/he/common.json';
import heSettings from './locales/he/settings.json';
import heProjects from './locales/he/projects.json';
import heTasks from './locales/he/tasks.json';
import heOrganization from './locales/he/organization.json';

const resources = {
en: {
Expand Down Expand Up @@ -90,6 +99,13 @@ const resources = {
tasks: zhHantTasks,
organization: zhHantOrganization,
},
he: {
common: heCommon,
settings: heSettings,
projects: heProjects,
tasks: heTasks,
organization: heOrganization,
},
};

i18n
Expand Down Expand Up @@ -135,21 +151,30 @@ if (import.meta.env.DEV) {

// Function to update language from config
export const updateLanguageFromConfig = (configLanguage: string) => {
const applyLanguage = (language: string) => {
i18n.changeLanguage(language).then(() => {
const resolvedLanguage =
i18n.resolvedLanguage ?? i18n.language ?? language;
updateDocumentDirection(resolvedLanguage);
});
};

if (configLanguage === 'BROWSER') {
// Use browser detection
const detected = i18n.services.languageDetector?.detect();
const detectedLang = Array.isArray(detected) ? detected[0] : detected;
i18n.changeLanguage(detectedLang || 'en');
const lang = detectedLang || 'en';
applyLanguage(lang);
} else {
// Use explicit language selection with proper mapping
const langCode = uiLanguageToI18nCode(configLanguage);
if (langCode) {
i18n.changeLanguage(langCode);
applyLanguage(langCode);
} else {
console.warn(
`Unknown UI language: ${configLanguage}, falling back to 'en'`
);
i18n.changeLanguage('en');
applyLanguage('en');
}
}
};
Expand Down
23 changes: 23 additions & 0 deletions packages/web-core/src/i18n/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const UI_TO_I18N = {
KO: 'ko',
ZH_HANS: 'zh-Hans',
ZH_HANT: 'zh-Hant',
HE: 'he',
} as const;

const SUPPORTED_UI_LANGUAGES = [
Expand All @@ -23,6 +24,7 @@ const SUPPORTED_UI_LANGUAGES = [
'KO',
'ZH_HANS',
'ZH_HANT',
'HE',
] as const;
export const SUPPORTED_I18N_CODES = Object.values(UI_TO_I18N);

Expand All @@ -34,6 +36,7 @@ const FALLBACK_ENDONYMS = {
ko: '한국어',
'zh-Hans': '简体中文',
'zh-Hant': '繁體中文',
he: 'עברית',
} as const;

/**
Expand Down Expand Up @@ -74,3 +77,23 @@ export function getLanguageOptions(browserDefaultLabel: string) {
: getEndonym(UI_TO_I18N[ui as keyof typeof UI_TO_I18N]),
}));
}

/**
* Language codes that use right-to-left text direction.
*/
const RTL_LANGUAGE_CODES = ['he'];

/**
* Returns true if the given i18n language code uses RTL text direction.
*/
export function isRtlLanguage(langCode: string): boolean {
return RTL_LANGUAGE_CODES.some((rtl) => langCode.startsWith(rtl));
}

/**
* Update the document's text direction based on the active language.
*/
export function updateDocumentDirection(langCode: string): void {
document.documentElement.dir = isRtlLanguage(langCode) ? 'rtl' : 'ltr';
document.documentElement.lang = langCode;
}
32 changes: 32 additions & 0 deletions packages/web-core/src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,38 @@
"yes": "Yes",
"no": "No"
},
"onboardingLanding": {
"codingAgent": "Coding Agent",
"codeEditor": "Code Editor",
"customCommand": "Custom Command",
"customCommandPlaceholder": "e.g. code --wait",
"notificationSound": "Notification Sound",
"noSound": "No sound",
"actions": {
"saving": "Saving...",
"continue": "Continue"
},
"safetyNotice": {
"beforeFlags": "Vibe Kanban runs AI coding agents with",
"afterFlags": "by default. Always review what agents are doing.",
"learnMore": "Learn more"
},
"footer": {
"beforeTerms": "By continuing you agree to the",
"termsAndConditions": "terms and conditions",
"and": "and",
"privacyPolicy": "privacy policy"
},
"soundOptions": {
"abstract1": "Abstract Sound 1",
"abstract2": "Abstract Sound 2",
"abstract3": "Abstract Sound 3",
"abstract4": "Abstract Sound 4",
"cowMooing": "Cow Mooing",
"phoneVibration": "Phone Vibration",
"rooster": "Rooster"
}
},
"toolbar": {
"sortBy": "Sort by",
"groupBy": "Group by"
Expand Down
Loading