feat: language selector on user creation, pass to welcome email
Some checks failed
Deploy / deploy (push) Failing after 9s

Adds preferred_language field to Users, language picker (EN/AR) in
create/edit user form, persists to NocoDB, and passes it to the
welcome notification so new users receive emails in their language.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-09 13:14:32 +03:00
parent 8c69f1846f
commit 5e47d11e32
4 changed files with 30 additions and 4 deletions

View File

@@ -570,6 +570,7 @@
"users.passwordMismatch": "كلمات المرور غير متطابقة", "users.passwordMismatch": "كلمات المرور غير متطابقة",
"users.passwordRequired": "كلمة المرور مطلوبة للمستخدمين الجدد", "users.passwordRequired": "كلمة المرور مطلوبة للمستخدمين الجدد",
"users.saveFailed": "فشل في حفظ المستخدم", "users.saveFailed": "فشل في حفظ المستخدم",
"users.preferredLanguage": "اللغة المفضلة",
"users.deleteFailed": "فشل في حذف المستخدم", "users.deleteFailed": "فشل في حذف المستخدم",
"settings.saveFailed": "فشل في الحفظ", "settings.saveFailed": "فشل في الحفظ",

View File

@@ -570,6 +570,7 @@
"users.passwordMismatch": "Passwords do not match", "users.passwordMismatch": "Passwords do not match",
"users.passwordRequired": "Password is required for new users", "users.passwordRequired": "Password is required for new users",
"users.saveFailed": "Failed to save user", "users.saveFailed": "Failed to save user",
"users.preferredLanguage": "Preferred Language",
"users.deleteFailed": "Failed to delete user", "users.deleteFailed": "Failed to delete user",
"settings.saveFailed": "Failed to save", "settings.saveFailed": "Failed to save",

View File

@@ -14,7 +14,7 @@ const ROLES = [
] ]
const EMPTY_FORM = { const EMPTY_FORM = {
name: '', email: '', password: '', role: 'contributor', avatar: '', name: '', email: '', password: '', role: 'contributor', avatar: '', preferred_language: 'en',
} }
function RoleBadge({ role }) { function RoleBadge({ role }) {
@@ -66,6 +66,7 @@ export default function Users() {
email: form.email, email: form.email,
role: form.role, role: form.role,
avatar: form.avatar || null, avatar: form.avatar || null,
preferred_language: form.preferred_language || 'en',
} }
if (form.password) data.password = form.password if (form.password) data.password = form.password
@@ -97,6 +98,7 @@ export default function Users() {
password: '', password: '',
role: user.role || 'contributor', role: user.role || 'contributor',
avatar: user.avatar || '', avatar: user.avatar || '',
preferred_language: user.preferred_language || 'en',
}) })
setConfirmPassword('') setConfirmPassword('')
setPasswordError('') setPasswordError('')
@@ -312,6 +314,27 @@ export default function Users() {
</div> </div>
</div> </div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('users.preferredLanguage')}</label>
<div className="grid grid-cols-2 gap-2">
{[{ value: 'en', label: 'English', flag: '🇬🇧' }, { value: 'ar', label: 'العربية', flag: '🇸🇦' }].map(l => (
<button
key={l.value}
type="button"
onClick={() => setForm(f => ({ ...f, preferred_language: l.value }))}
className={`p-2.5 rounded-lg border-2 text-center transition-all ${
form.preferred_language === l.value
? 'border-brand-primary bg-brand-primary/5'
: 'border-border hover:border-brand-primary/30'
}`}
>
<span className="text-lg">{l.flag}</span>
<span className="text-xs font-medium text-text-primary ms-1.5">{l.label}</span>
</button>
))}
</div>
</div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border"> <div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
<button <button
onClick={() => { setShowModal(false); setEditingUser(null) }} onClick={() => { setShowModal(false); setEditingUser(null) }}

View File

@@ -947,7 +947,7 @@ app.get('/api/users/team', requireAuth, async (req, res) => {
}); });
app.post('/api/users/team', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => { app.post('/api/users/team', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
const { name, email, password, team_role, brands, phone, role, role_id, avatar } = req.body; const { name, email, password, team_role, brands, phone, role, role_id, avatar, preferred_language } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' }); if (!name) return res.status(400).json({ error: 'Name is required' });
if (!email) return res.status(400).json({ error: 'Email is required' }); if (!email) return res.status(400).json({ error: 'Email is required' });
@@ -972,11 +972,12 @@ app.post('/api/users/team', requireAuth, requireRole('superadmin', 'manager'), a
password_hash: passwordHash, password_hash: passwordHash,
role_id: role_id || null, role_id: role_id || null,
avatar: avatar || null, avatar: avatar || null,
preferred_language: preferred_language || 'en',
}); });
const user = await nocodb.get('Users', created.Id); const user = await nocodb.get('Users', created.Id);
res.status(201).json(stripSensitiveFields({ ...user, id: user.Id, _id: user.Id })); res.status(201).json(stripSensitiveFields({ ...user, id: user.Id, _id: user.Id }));
notify.notifyUserInvited({ email, name, password: defaultPassword, inviterName: req.session.userName }); notify.notifyUserInvited({ email, name, password: defaultPassword, inviterName: req.session.userName, lang: preferred_language || 'en' });
} catch (err) { } catch (err) {
console.error('Create team member error:', err); console.error('Create team member error:', err);
res.status(500).json({ error: 'Failed to create team member' }); res.status(500).json({ error: 'Failed to create team member' });
@@ -989,7 +990,7 @@ app.patch('/api/users/team/:id', requireAuth, requireRole('superadmin', 'manager
if (!existing) return res.status(404).json({ error: 'User not found' }); if (!existing) return res.status(404).json({ error: 'User not found' });
const data = {}; const data = {};
for (const f of ['name', 'email', 'team_role', 'phone', 'avatar']) { for (const f of ['name', 'email', 'team_role', 'phone', 'avatar', 'preferred_language']) {
if (req.body[f] !== undefined) data[f] = req.body[f]; if (req.body[f] !== undefined) data[f] = req.body[f];
} }
if (req.body.brands !== undefined) data.brands = JSON.stringify(req.body.brands); if (req.body.brands !== undefined) data.brands = JSON.stringify(req.body.brands);