diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ff462519..4f27f1cf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,8 +1,11 @@ name: CI on: + # Every pull request, not just the ones targeting main: stacked PRs (a chain + # of branches merged bottom-up at the end) target their parent branch, and + # with a `branches: [main]` filter they would ride all the way to the final + # merge without a single CI run. pull_request: - branches: [main] push: branches: [main] diff --git a/app/i18n.sync.json b/app/i18n.sync.json index ba9dd67e..50e804a0 100644 --- a/app/i18n.sync.json +++ b/app/i18n.sync.json @@ -79,6 +79,48 @@ "template_image": "140d5d5895d7", "template_name": "f95e97c2f767", "templates": "56b564b75c7f" + }, + "user/auth:ru": { + "account.closed_body": "0be53513ddac", + "account.closed_cta": "27a0d553bf29", + "account.closed_title": "dd5e053a970d", + "account.create_account": "86033f75a4c0", + "account.create_subtitle": "c5aff35f38e9", + "account.create_title": "9e709348f582", + "account.email_invalid": "c6e81fdf78b3", + "account.email_label": "969ccbd3cf63", + "account.email_placeholder": "9a17bdc59659", + "account.email_required": "aae929110124", + "account.have_account": "e77fea936d3e", + "account.login_failed": "5a96e8452f1e", + "account.name_label": "dcd1d5223f73", + "account.name_placeholder": "01332c876518", + "account.name_required": "604fd1b2e29c", + "account.need_access": "ab1a55b13fa2", + "account.new_here": "38c1c4457cd1", + "account.password_hide": "a60a56c584b3", + "account.password_hint": "e4e54a0028c8", + "account.password_label": "e7cf3ef4f17c", + "account.password_required": "7aeae1412484", + "account.password_show": "6aeaa6a53d09", + "account.password_too_short": "977f3b2676a9", + "account.register_failed": "d40e3b08ef50", + "account.sign_in": "bfd402b2f6f3", + "account.submit_login": "bfd402b2f6f3", + "account.submit_login_pending": "8b9d3de8ae00", + "account.submit_register": "798ca2ce18bf", + "account.submit_register_pending": "f2daf61e527b", + "account.welcome_subtitle": "c2eae8162dd8", + "account.welcome_title": "6621249514b7", + "promo.back_to_landing": "a0653cf86b3c", + "promo.badge": "de065a5c850b", + "promo.feature_deploy": "b8d686e626c7", + "promo.feature_state": "dcb893a52450", + "promo.feature_streaming": "eb803528b70d", + "promo.footer": "8f3bab563458", + "promo.lede": "0877d8f80ff2", + "promo.title": "9f28248b81cc", + "promo.title_accent": "09659d3fbc68" } } } diff --git a/app/slices/user/auth/components/auth/common/Form.vue b/app/slices/user/auth/components/auth/common/Form.vue index b5053ba7..482d6ee3 100644 --- a/app/slices/user/auth/components/auth/common/Form.vue +++ b/app/slices/user/auth/components/auth/common/Form.vue @@ -4,7 +4,10 @@ type Mode = 'login' | 'register'; const props = defineProps<{ mode: Mode; submitting?: boolean; + /** Human text from the API — already English, not a key. Null when the call + * failed without a usable message, in which case `failed` drives our copy. */ errorMessage?: string | null; + failed?: boolean; registrationEnabled?: boolean; }>(); @@ -13,6 +16,9 @@ const emit = defineEmits<{ }>(); const form = reactive({ name: '', email: '', password: '' }); +// Validation state holds i18n KEYS, not sentences: the template renders them +// with $t. Keeping the text here would hide user-visible copy inside branching +// logic, where the next extraction pass would never find it. const errors = reactive< Partial> >({}); @@ -24,21 +30,23 @@ const showPassword = ref(false); function validateField(field: 'name' | 'email' | 'password') { if (field === 'name') { errors.name = - props.mode === 'register' && !form.name.trim() ? 'Name is required' : undefined; + props.mode === 'register' && !form.name.trim() + ? 'account.name_required' + : undefined; return; } if (field === 'email') { const email = form.email.trim(); - if (!email) errors.email = 'Email is required'; + if (!email) errors.email = 'account.email_required'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) - errors.email = 'Enter a valid email'; + errors.email = 'account.email_invalid'; else errors.email = undefined; return; } if (field === 'password') { - if (!form.password) errors.password = 'Password is required'; + if (!form.password) errors.password = 'account.password_required'; else if (props.mode === 'register' && form.password.length < 8) - errors.password = 'At least 8 characters'; + errors.password = 'account.password_too_short'; else errors.password = undefined; } } @@ -65,24 +73,29 @@ function onSubmit() { }); } -const title = computed(() => - props.mode === 'login' ? 'Welcome back' : 'Create your account', +const titleKey = computed(() => + props.mode === 'login' ? 'account.welcome_title' : 'account.create_title', ); -const subtitle = computed(() => +const subtitleKey = computed(() => props.mode === 'login' - ? 'Sign in to manage your agents.' - : 'Get started with your AI workers.', + ? 'account.welcome_subtitle' + : 'account.create_subtitle', ); -const submitLabel = computed(() => - props.submitting - ? props.mode === 'login' - ? 'Signing in…' - : 'Creating account…' - : props.mode === 'login' - ? 'Sign in' - : 'Create account', +const submitLabelKey = computed(() => { + if (props.submitting) { + return props.mode === 'login' + ? 'account.submit_login_pending' + : 'account.submit_register_pending'; + } + return props.mode === 'login' + ? 'account.submit_login' + : 'account.submit_register'; +}); + +const failureKey = computed(() => + props.mode === 'login' ? 'account.login_failed' : 'account.register_failed', ); const inputClass = (field: 'name' | 'email' | 'password') => @@ -103,8 +116,8 @@ function showError(field: 'name' | 'email' | 'password') {