From 718dd3e361914dac80c6cc415cfe66c3bf858cb1 Mon Sep 17 00:00:00 2001 From: AlexAnh Date: Sun, 4 Oct 2026 06:49:49 +0700 Subject: [PATCH 1/2] feat(i18n): wrap hardcoded UI strings and update Vietnamese translations --- backend/lang/en.json | 25 ++++++++++- backend/lang/vi.json | 25 ++++++++++- .../Pages/admin/connectivity/mqtt/Index.jsx | 44 +++++++++---------- .../connectivity/mqtt/MqttConnectionForm.jsx | 30 ++++++------- .../js/Pages/admin/connectivity/mqtt/Show.jsx | 38 ++++++++-------- .../js/Pages/admin/connectivity/ui.jsx | 7 +-- .../js/Pages/admin/logs/Activity.jsx | 30 ++++++------- .../resources/js/Pages/admin/logs/System.jsx | 6 +-- .../js/Pages/admin/material-lots/Show.jsx | 6 +-- .../js/Pages/admin/reports/BatchReport.jsx | 6 +-- .../js/Pages/admin/schedule/Index.jsx | 8 ++-- .../js/Pages/operator/WorkOrderDetail.jsx | 8 ++-- .../js/Pages/operator/Workstation.jsx | 10 ++--- mobile/components/admin/CompanyForm.tsx | 20 ++++----- mobile/components/admin/CostSourceForm.tsx | 18 ++++---- mobile/components/admin/CrewForm.tsx | 12 ++--- mobile/components/admin/LotSequenceForm.tsx | 14 +++--- mobile/components/admin/ShiftForm.tsx | 14 +++--- mobile/components/admin/UserForm.tsx | 6 +-- .../production/BomRequirementsCard.tsx | 10 +++-- .../production/QualityChecksCard.tsx | 20 +++++---- mobile/lang/en.json | 25 ++++++++++- mobile/lang/vi.json | 25 ++++++++++- 23 files changed, 252 insertions(+), 155 deletions(-) diff --git a/backend/lang/en.json b/backend/lang/en.json index 68c2cf1e2..9b9874151 100644 --- a/backend/lang/en.json +++ b/backend/lang/en.json @@ -6877,5 +6877,28 @@ "Module \":name\" could not be uninstalled: :msg": "Module \":name\" could not be uninstalled: :msg", "Module \":name\" uninstalled. Its database tables were left in place; reinstalling keeps the data.": "Module \":name\" uninstalled. Its database tables were left in place; reinstalling keeps the data.", "Failed to import settings - nothing from the file was saved. :message": "Failed to import settings - nothing from the file was saved. :message", - "Click a step to show only its components; click it again to show all. The number on a step is how many components it consumes.": "Click a step to show only its components; click it again to show all. The number on a step is how many components it consumes." + "Click a step to show only its components; click it again to show all. The number on a step is how many components it consumes.": "Click a step to show only its components; click it again to show all. The number on a step is how many components it consumes.", + "(inactive)": "(inactive)", + "The production line this device feeds — the default target for its topic mappings (e.g. a sensor counting units on this line).": "The production line this device feeds — the default target for its topic mappings (e.g. a sensor counting units on this line).", + "Also count as finished goods (feeds the work order's produced qty)": "Also count as finished goods (feeds the work order's produced qty)", + "Counting": "Counting", + "(supports + and # wildcards)": "(supports + and # wildcards)", + "e.g. Production count from Line 1": "e.g. Production count from Line 1", + "Field path — e.g. $.qty or $.data.value": "Field path — e.g. $.qty or $.data.value", + "Action type *": "Action type *", + "Condition — e.g. value > 0": "Condition — e.g. value > 0", + "Counting — pick the line and station": "Counting — pick the line and station", + "Action params (JSON) — e.g. {\"order_no_path\":\"$.order_no\"}": "Action params (JSON) — e.g. {\"order_no_path\":\"$.order_no\"}", + "e.g. Update produced qty from machine counter": "e.g. Update produced qty from machine counter", + "· scale ×": "· scale ×", + "Upstream source batch": "Upstream source batch", + "Add Produced Quantity": "Add Produced Quantity", + "Already produced": "Already produced", + "No rows yet.": "No rows yet.", + "est.": "est.", + "Contact": "Contact", + "Identity": "Identity", + "Pricing": "Pricing", + "Sequence": "Sequence", + "Credentials": "Credentials" } diff --git a/backend/lang/vi.json b/backend/lang/vi.json index 513c708d0..9748cb253 100644 --- a/backend/lang/vi.json +++ b/backend/lang/vi.json @@ -6990,5 +6990,28 @@ "Module \":name\" could not be uninstalled: :msg": "Không thể gỡ cài đặt mô-đun \":name\": :msg", "Module \":name\" uninstalled. Its database tables were left in place; reinstalling keeps the data.": "Đã gỡ cài đặt mô-đun \":name\". Các bảng cơ sở dữ liệu vẫn được giữ nguyên; cài đặt lại sẽ khôi phục dữ liệu.", "Failed to import settings - nothing from the file was saved. :message": "Không thể nhập cài đặt - không có dữ liệu nào từ tệp được lưu. :message", - "Click a step to show only its components; click it again to show all. The number on a step is how many components it consumes.": "Bấm vào một bước công đoạn để chỉ hiển thị các linh kiện của bước đó; bấm lần nữa để hiển thị tất cả. Con số trên bước công đoạn cho biết số lượng linh kiện tiêu hao." + "Click a step to show only its components; click it again to show all. The number on a step is how many components it consumes.": "Bấm vào một bước công đoạn để chỉ hiển thị các linh kiện của bước đó; bấm lần nữa để hiển thị tất cả. Con số trên bước công đoạn cho biết số lượng linh kiện tiêu hao.", + "(inactive)": "(không hoạt động)", + "The production line this device feeds — the default target for its topic mappings (e.g. a sensor counting units on this line).": "Dây chuyền sản xuất mà thiết bị này cung cấp dữ liệu — mục tiêu mặc định cho các ánh xạ chủ đề (ví dụ: cảm biến đếm số lượng trên dây chuyền này).", + "Also count as finished goods (feeds the work order's produced qty)": "Đồng thời tính là thành phẩm (cập nhật vào số lượng đã sản xuất của lệnh sản xuất)", + "Counting": "Đếm sản lượng", + "(supports + and # wildcards)": "(hỗ trợ ký tự đại diện + và #)", + "e.g. Production count from Line 1": "vd: Đếm sản lượng từ Dây chuyền 1", + "Field path — e.g. $.qty or $.data.value": "Đường dẫn trường — vd: $.qty hoặc $.data.value", + "Action type *": "Loại hành động *", + "Condition — e.g. value > 0": "Điều kiện — vd: value > 0", + "Counting — pick the line and station": "Đếm sản lượng — chọn dây chuyền và công đoạn", + "Action params (JSON) — e.g. {\"order_no_path\":\"$.order_no\"}": "Tham số hành động (JSON) — vd: {\"order_no_path\":\"$.order_no\"}", + "e.g. Update produced qty from machine counter": "vd: Cập nhật sản lượng từ bộ đếm máy", + "· scale ×": "· hệ số tỉ lệ ×", + "Upstream source batch": "Lô sản xuất nguồn đầu vào", + "Add Produced Quantity": "Thêm sản lượng đã sản xuất", + "Already produced": "Đã sản xuất", + "No rows yet.": "Chưa có dòng nào.", + "est.": "ước tính", + "Contact": "Liên hệ", + "Identity": "Định danh", + "Pricing": "Định giá", + "Sequence": "Quy tắc đánh số", + "Credentials": "Thông tin xác thực" } diff --git a/backend/resources/js/Pages/admin/connectivity/mqtt/Index.jsx b/backend/resources/js/Pages/admin/connectivity/mqtt/Index.jsx index fcb5b664f..f62eb3296 100644 --- a/backend/resources/js/Pages/admin/connectivity/mqtt/Index.jsx +++ b/backend/resources/js/Pages/admin/connectivity/mqtt/Index.jsx @@ -4,7 +4,7 @@ import AppDataTable from '../../../../components/AppDataTable'; import AppLayout from '../../../../layouts/AppLayout'; import Tooltip from '../../../../components/Tooltip'; import useConfirm from '../../../../components/useConfirm'; -import { formatNumber } from '../../../../lib/i18n'; +import { __, formatNumber } from '../../../../lib/i18n'; const STATUS_DOT = { green: 'bg-om-running', @@ -18,7 +18,7 @@ export default function MqttIndex() { const { confirm, dialog } = useConfirm(); const handleDelete = (conn) => { - confirm({ title: 'Delete this connection and all its topics?' }, () => { + confirm({ title: __('Delete this connection and all its topics?') }, () => { router.delete(`/admin/connectivity/mqtt/${conn.id}`, { preserveScroll: true }); }); }; @@ -27,7 +27,7 @@ export default function MqttIndex() { { id: 'status', accessorKey: 'status', - header: 'Status', + header: __('Status'), cell: ({ row }) => { const conn = row.original; const dot = STATUS_DOT[conn.status_color] ?? 'bg-slate-400'; @@ -43,7 +43,7 @@ export default function MqttIndex() { { id: 'name', accessorKey: 'name', - header: 'Name', + header: __('Name'), cell: ({ row }) => { const conn = row.original; return ( @@ -52,7 +52,7 @@ export default function MqttIndex() { {conn.name} {!conn.is_active && ( - (inactive) + {__('(inactive)')} )} ); @@ -62,7 +62,7 @@ export default function MqttIndex() { { id: 'broker', accessorFn: (r) => (r.mqtt_host ? `${r.mqtt_host}:${r.mqtt_port}` : ''), - header: 'Broker', + header: __('Broker'), cell: ({ row }) => { const conn = row.original; return ( @@ -75,7 +75,7 @@ export default function MqttIndex() { )} ) : ( - Not configured + {__('Not configured')} )} ); @@ -85,14 +85,14 @@ export default function MqttIndex() { { id: 'topics', accessorKey: 'topics_count', - header: 'Topics', + header: __('Topics'), cell: ({ row }) => {row.original.topics_count}, meta: { align: 'left' }, }, { id: 'messages', accessorKey: 'messages_received', - header: 'Messages', + header: __('Messages'), cell: ({ row }) => ( {formatNumber(Number(row.original.messages_received))} ), @@ -101,7 +101,7 @@ export default function MqttIndex() { { id: 'last_connected', accessorKey: 'last_connected_at', - header: 'Last connected', + header: __('Last connected'), cell: ({ row }) => ( {row.original.last_connected_at ?? '—'} ), @@ -109,33 +109,33 @@ export default function MqttIndex() { }, { id: 'actions', - header: 'Actions', + header: __('Actions'), enableSorting: false, cell: ({ row }) => { const conn = row.original; return (
- - + + - - + + - + - Cancel + {__('Cancel')} {onDelete && ( )}
diff --git a/backend/resources/js/Pages/admin/connectivity/mqtt/Show.jsx b/backend/resources/js/Pages/admin/connectivity/mqtt/Show.jsx index a74b00e83..94a80898f 100644 --- a/backend/resources/js/Pages/admin/connectivity/mqtt/Show.jsx +++ b/backend/resources/js/Pages/admin/connectivity/mqtt/Show.jsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { Head, router, usePage, useForm } from '@inertiajs/react'; import { Button, Checkbox, Dropdown } from '@openmes/ui'; import AppLayout from '../../../../layouts/AppLayout'; -import { formatNumber, formatTime } from '../../../../lib/i18n'; +import { __, formatNumber, formatTime } from '../../../../lib/i18n'; import useConfirm from '../../../../components/useConfirm'; import { nameControl } from '../../../../lib/fieldName'; @@ -460,7 +460,7 @@ function CountStepFields({ value, onChange }) { set({ also_count_work_order: next })} - label="Also count as finished goods (feeds the work order's produced qty)" + label={__("Also count as finished goods (feeds the work order's produced qty)")} /> @@ -520,8 +520,8 @@ function EditMappingForm({ mapping, topic, connectionId, onClose }) { form.setData('description', e.target.value)} className="w-full px-2 py-1 text-xs border border-om-line rounded bg-om-card text-om-ink focus:ring-1 focus:ring-om-accent" />
- - + +
@@ -550,14 +550,14 @@ function AddTopicForm({ connectionId }) { - Add topic + {__('Add topic')} {open && (
- Topic pattern (supports + and # wildcards) + {__('Topic pattern')} {__('(supports + and # wildcards)')}
-
Payload format
+
{__('Payload format')}
form.setData('payload_format', v)} options={[ @@ -585,22 +585,22 @@ function AddTopicForm({ connectionId }) {
-
Description (optional)
+
{__('Description (optional)')}
form.setData('description', e.target.value)} - placeholder="e.g. Production count from Line 1" + placeholder={__('e.g. Production count from Line 1')} className="w-full px-3 py-2 text-sm border border-om-line rounded-om-sm bg-om-card text-om-ink focus:ring-2 focus:ring-om-accent focus:border-transparent" />
@@ -637,7 +637,7 @@ function AddMappingForm({ connectionId, topicId, onClose }) { form.setData('action_type', v)} - options={Object.entries(ACTION_LABELS).map(([val, lbl]) => ({ value: val, label: lbl }))} + options={Object.entries(ACTION_LABELS).map(([val, lbl]) => ({ value: val, label: __(lbl) }))} className="w-full" /> @@ -658,11 +658,11 @@ function AddMappingForm({ connectionId, topicId, onClose }) { )} - form.setData('description', e.target.value)} placeholder="e.g. Update produced qty from machine counter" className="w-full px-2 py-1.5 text-xs border border-om-line rounded-om-sm bg-om-card text-om-ink focus:ring-2 focus:ring-om-accent focus:border-transparent" /> + form.setData('description', e.target.value)} placeholder={__('e.g. Update produced qty from machine counter')} className="w-full px-2 py-1.5 text-xs border border-om-line rounded-om-sm bg-om-card text-om-ink focus:ring-2 focus:ring-om-accent focus:border-transparent" />
- - + +
); @@ -671,8 +671,8 @@ function AddMappingForm({ connectionId, topicId, onClose }) { function MiniField({ label, children }) { return (
-
{label}
- {nameControl(children, label)} +
{__(label)}
+ {nameControl(children, __(label))}
); } diff --git a/backend/resources/js/Pages/admin/connectivity/ui.jsx b/backend/resources/js/Pages/admin/connectivity/ui.jsx index 7ee46ad2e..a27403f07 100644 --- a/backend/resources/js/Pages/admin/connectivity/ui.jsx +++ b/backend/resources/js/Pages/admin/connectivity/ui.jsx @@ -3,6 +3,7 @@ * "All" overview). Extracted to avoid copy-pasting the status dot, stat card, * and form section/field helpers across every page. */ +import { __ } from '../../../lib/i18n'; import { nameControl } from '../../../lib/fieldName'; export const STATUS_DOT = { @@ -23,7 +24,7 @@ export function StatCard({ value, label, capitalize = false }) { return (

{value}

-

{label}

+

{__(label)}

); } @@ -32,7 +33,7 @@ export function StatCard({ value, label, capitalize = false }) { export function Section({ title, children }) { return (
-

{title}

+

{__(title)}

{children}
); @@ -43,7 +44,7 @@ export function Field({ label, required, error, children }) { return (
- {label} {required && *} + {__(label)} {required && *}
{nameControl(children, label)} {error &&

{error}

} diff --git a/backend/resources/js/Pages/admin/logs/Activity.jsx b/backend/resources/js/Pages/admin/logs/Activity.jsx index 617f3ac76..91a7eb985 100644 --- a/backend/resources/js/Pages/admin/logs/Activity.jsx +++ b/backend/resources/js/Pages/admin/logs/Activity.jsx @@ -91,37 +91,37 @@ function DetailModal({ log, onClose }) {
- Timestamp:{' '} + {__('Timestamp')}:{' '} {formatTs(log.created_at)}
- Source:{' '} + {__('Source')}:{' '} {log.source || '—'}
- User:{' '} + {__('User')}:{' '} {log.user?.name ?? 'Guest'}
- IP address:{' '} + {__('IP address')}:{' '} {log.ip_address || '—'}
{log.source === 'audit' && (
- Action:{' '} + {__('Action')}:{' '} {log.action || '—'}
- Entity:{' '} + {__('Entity')}:{' '} {entityLabel(log) || '—'}
{log.before_state && (
- Before state + {__('Before state')}
                                         {prettyJson(log.before_state)}
                                     
@@ -129,7 +129,7 @@ function DetailModal({ log, onClose }) { )} {log.after_state && (
- After state + {__('After state')}
                                         {prettyJson(log.after_state)}
                                     
@@ -141,28 +141,28 @@ function DetailModal({ log, onClose }) { {log.source === 'request' && (
- Method:{' '} + {__('Method')}:{' '} {log.method || '—'}
- Path:{' '} + {__('Path')}:{' '} {log.path || '—'}
- Route name:{' '} + {__('Route name')}:{' '} {log.route_name || '—'}
- Status:{' '} + {__('Status')}:{' '} {log.status ?? '—'}
- Duration:{' '} + {__('Duration')}:{' '} {log.duration_ms != null ? `${log.duration_ms} ms` : '—'}
- Sampled:{' '} - {log.sampled ? 'yes' : 'no'} + {__('Sampled')}:{' '} + {log.sampled ? __('yes') : __('no')}
)} diff --git a/backend/resources/js/Pages/admin/logs/System.jsx b/backend/resources/js/Pages/admin/logs/System.jsx index b5713e5ae..7d56c4826 100644 --- a/backend/resources/js/Pages/admin/logs/System.jsx +++ b/backend/resources/js/Pages/admin/logs/System.jsx @@ -189,17 +189,17 @@ function AppTab({ entries: initialEntries, availableDates, date, level, search } {live ? ( <>
diff --git a/backend/resources/js/Pages/admin/material-lots/Show.jsx b/backend/resources/js/Pages/admin/material-lots/Show.jsx index 4e4c7dd0c..7571ef71e 100644 --- a/backend/resources/js/Pages/admin/material-lots/Show.jsx +++ b/backend/resources/js/Pages/admin/material-lots/Show.jsx @@ -228,7 +228,7 @@ export default function MaterialLotShow({ lot }) { {lot.sublots && lot.sublots.length > 0 && (

- Sublots ({lot.sublots.length}) + {__('Sublots')} ({lot.sublots.length})

{sourceBatchId && (

- Upstream source batch:{' '} + {__('Upstream source batch')}:{' '} #{sourceBatchId} - {' '}— see backward genealogy API for full chain. + {' '}— {__('see backward genealogy API for full chain.')}

)}
diff --git a/backend/resources/js/Pages/admin/reports/BatchReport.jsx b/backend/resources/js/Pages/admin/reports/BatchReport.jsx index 986c35d2c..f4174784a 100644 --- a/backend/resources/js/Pages/admin/reports/BatchReport.jsx +++ b/backend/resources/js/Pages/admin/reports/BatchReport.jsx @@ -176,11 +176,11 @@ export default function BatchReport() { {qualityChecks.map((qc, qi) => (
- Check #{qi + 1} + {__('Check')} #{qi + 1} {qc.checked_at} - By: {qc.checked_by?.name ?? '—'} + {__('By')}: {qc.checked_by?.name ?? '—'} {qc.production_quantity != null && ( - Production: {Number(qc.production_quantity).toFixed(0)} pcs + {__('Production')}: {Number(qc.production_quantity).toFixed(0)} {__('pcs')} )}
diff --git a/backend/resources/js/Pages/admin/schedule/Index.jsx b/backend/resources/js/Pages/admin/schedule/Index.jsx index f9609fd85..7ca28b87c 100644 --- a/backend/resources/js/Pages/admin/schedule/Index.jsx +++ b/backend/resources/js/Pages/admin/schedule/Index.jsx @@ -79,12 +79,12 @@ export default function ScheduleIndex() { {/* Header */}
-

Production Schedule

+

{__('Production Schedule')}

- Week {weekStartFmt} – {weekEndFmt} + {__('Week')} {weekStartFmt} – {weekEndFmt} {currentShift && ( -  · Current shift: {currentShift.name} ({currentShift.start_time?.slice(0, 5)}–{currentShift.end_time?.slice(0, 5)}) +  · {__('Current shift')}: {currentShift.name} ({currentShift.start_time?.slice(0, 5)}–{currentShift.end_time?.slice(0, 5)}) )}

@@ -92,7 +92,7 @@ export default function ScheduleIndex() { {/* Filters */}
- +
); diff --git a/backend/resources/js/Pages/admin/schedule/Index.jsx b/backend/resources/js/Pages/admin/schedule/Index.jsx index 7ca28b87c..dab4e8544 100644 --- a/backend/resources/js/Pages/admin/schedule/Index.jsx +++ b/backend/resources/js/Pages/admin/schedule/Index.jsx @@ -96,7 +96,7 @@ export default function ScheduleIndex() { @@ -113,17 +113,17 @@ export default function ScheduleIndex() { value={lineId == null ? '' : String(lineId)} onChange={(v) => navigate({ week: weekStart ? fmtWeek(new Date(weekStart)) : '', line_id: v })} options={[ - { value: '', label: 'All Lines' }, + { value: '', label: __('All Lines') }, ...lines.map((l) => ({ value: String(l.id), label: l.name })), ]} className="w-full" /> - + diff --git a/mobile/components/admin/CostSourceForm.tsx b/mobile/components/admin/CostSourceForm.tsx index d35fb0daf..26cebfc00 100644 --- a/mobile/components/admin/CostSourceForm.tsx +++ b/mobile/components/admin/CostSourceForm.tsx @@ -1,5 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { View } from 'react-native'; import { z } from 'zod'; @@ -31,6 +32,7 @@ interface Props { } export function CostSourceForm({ initial, mode, onSubmit, submitting }: Props) { + const { t } = useTranslation(); const { control, handleSubmit, formState: { isValid } } = useForm({ resolver: zodResolver(costSourceSchema), mode: 'onChange',