diff --git a/src/app/(main)/axios/axiosInstance.ts b/src/app/(main)/axios/axiosInstance.ts index 1d24e5d9..8de915aa 100644 --- a/src/app/(main)/axios/axiosInstance.ts +++ b/src/app/(main)/axios/axiosInstance.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { clientAcceptLanguage } from "@/i18n/locale"; const axiosInstance = axios.create({ baseURL: process.env.API_URL, @@ -32,6 +33,9 @@ export const SetupAxiosInterceptors = (inputToken: string | null) => { if (token) { config.headers.Authorization = `Bearer ${token}`; } + if (!config.headers["Accept-Language"] && !config.headers["accept-language"]) { + config.headers["Accept-Language"] = clientAcceptLanguage(); + } return config; }, (error) => { diff --git a/src/app/(main)/bagPrint/page.tsx b/src/app/(main)/bagPrint/page.tsx index 0afeae7b..afeb55a7 100644 --- a/src/app/(main)/bagPrint/page.tsx +++ b/src/app/(main)/bagPrint/page.tsx @@ -1,20 +1,18 @@ -import { I18nProvider } from "@/i18n"; +import { detectLanguage, I18nProvider } from "@/i18n"; import { Metadata } from "next"; -import BagPrintSearch from "@/components/BagPrint/BagPrintSearch"; -import { Stack, Typography } from "@mui/material"; +import BagPrintSearch, { + BagPrintPageTitle, +} from "@/components/BagPrint/BagPrintSearch"; -export const metadata: Metadata = { - title: "打袋機", -}; +export async function generateMetadata(): Promise { + const english = (await detectLanguage()).toLowerCase().startsWith("en"); + return { title: english ? "Bag printer" : "打袋機" }; +} export default async function BagPrintPage() { return ( - - - 打袋機 - - + ); diff --git a/src/app/(main)/chart/_components/ChartCard.tsx b/src/app/(main)/chart/_components/ChartCard.tsx index 43c561d9..5fb1ac1d 100644 --- a/src/app/(main)/chart/_components/ChartCard.tsx +++ b/src/app/(main)/chart/_components/ChartCard.tsx @@ -2,6 +2,7 @@ import { Card, CardContent, Typography, Stack, Button } from "@mui/material"; import FileDownload from "@mui/icons-material/FileDownload"; +import { useTranslation } from "react-i18next"; import { exportChartToXlsx } from "./exportChartToXlsx"; export default function ChartCard({ @@ -18,6 +19,7 @@ export default function ChartCard({ exportFilename?: string; exportData?: Record[]; }) { + const { t } = useTranslation("chart"); const handleExport = () => { if (exportFilename && exportData) { exportChartToXlsx(exportData, exportFilename); @@ -40,7 +42,7 @@ export default function ChartCard({ onClick={handleExport} sx={{ ml: "auto" }} > - 匯出 Excel + {t("exportExcel")} )} diff --git a/src/app/(main)/chart/_components/DateRangeSelect.tsx b/src/app/(main)/chart/_components/DateRangeSelect.tsx index eada7cf3..ad869a2c 100644 --- a/src/app/(main)/chart/_components/DateRangeSelect.tsx +++ b/src/app/(main)/chart/_components/DateRangeSelect.tsx @@ -1,28 +1,33 @@ "use client"; import { FormControl, InputLabel, Select, MenuItem } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import { RANGE_DAYS } from "./constants"; export default function DateRangeSelect({ value, onChange, - label = "日期範圍", + label, }: { value: number; onChange: (v: number) => void; label?: string; }) { + const { t } = useTranslation("chart"); + const L = useUiText(); + const resolvedLabel = label ?? L("日期範圍", "Date range"); return ( - {label} + {resolvedLabel} diff --git a/src/app/(main)/chart/delivery/page.tsx b/src/app/(main)/chart/delivery/page.tsx index 9aa9bbe1..9ed91087 100644 --- a/src/app/(main)/chart/delivery/page.tsx +++ b/src/app/(main)/chart/delivery/page.tsx @@ -26,21 +26,20 @@ import { type StaffOption, type TopDeliveryItemOption, } from "@/app/api/chart/client"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import ChartCard from "../_components/ChartCard"; import DateRangeSelect from "../_components/DateRangeSelect"; import { toDateRange, DEFAULT_RANGE_DAYS, TOP_ITEMS_LIMIT_OPTIONS } from "../_components/constants"; import SafeApexCharts from "@/components/charts/SafeApexCharts"; -const PAGE_TITLE = "發貨與配送"; +const STAFF_PERF_DATE_ERROR = "STAFF_PERF_DATE"; -const STAFF_PERF_STORE_FILTER_OPTIONS: { - value: StaffDeliveryPerformanceStoreFilter; - label: string; -}[] = [ - { value: "all", label: "全部" }, - { value: FLOOR_A, label: FLOOR_A }, - { value: FLOOR_B, label: FLOOR_B }, - { value: "null_only", label: "車線-X" }, +const STAFF_PERF_STORE_FILTER_VALUES: StaffDeliveryPerformanceStoreFilter[] = [ + "all", + FLOOR_A, + FLOOR_B, + "null_only", ]; type Criteria = { @@ -69,6 +68,8 @@ const defaultCriteria: Criteria = { /** FP-MTMS Version Checklist | Functions Ref. No. 61 | v1.0.0 | 2026-08-10 */ export default function DeliveryChartPage() { + const { t } = useTranslation("chart"); + const L = useUiText(); const [criteria, setCriteria] = useState(defaultCriteria); const [topItemsSelected, setTopItemsSelected] = useState([]); const [topItemOptions, setTopItemOptions] = useState([]); @@ -140,7 +141,7 @@ export default function DeliveryChartPage() { return; } if (s > e) { - setError("員工發貨績效的起始日期不能晚於結束日期"); + setError(STAFF_PERF_DATE_ERROR); setChartData((prev) => ({ ...prev, staffPerf: [] })); return; } @@ -207,18 +208,21 @@ export default function DeliveryChartPage() { return ( - {PAGE_TITLE} + {t("pageTitle_delivery")} {error && ( setError(null)}> - {error} + {error === STAFF_PERF_DATE_ERROR ? t("delivery_staffPerfDateError") : error} )} ({ 日期: d.date, 單數: d.orderCount }))} + title={t("delivery_ordersByDate")} + exportFilename={t("delivery_ordersByDate_export")} + exportData={chartData.delivery.map((d) => ({ + [L("日期", "Date")]: d.date, + [t("delivery_orderCount")]: d.orderCount, + }))} filters={ d.date) }, - yaxis: { title: { text: "單數" } }, + yaxis: { title: { text: t("delivery_orderCount") } }, plotOptions: { bar: { horizontal: false, columnWidth: "60%" } }, dataLabels: { enabled: false }, }} - series={[{ name: "單數", data: chartData.delivery.map((d) => d.orderCount) }]} + series={[{ name: t("delivery_orderCount"), data: chartData.delivery.map((d) => d.orderCount) }]} type="bar" width="100%" height={320} @@ -246,9 +250,13 @@ export default function DeliveryChartPage() { ({ 物料編碼: i.itemCode, 物料名稱: i.itemName, 數量: i.totalQty }))} + title={L("發貨數量排行(按物料)", "Top Items by Delivery Qty")} + exportFilename={L("發貨數量排行_按物料", "Delivery_Qty_By_Item")} + exportData={chartData.topItems.map((i) => ({ + [L("物料編碼", "Item Code")]: i.itemCode, + [L("物料名稱", "Item Name")]: i.itemName, + [t("warehouse_qty")]: i.totalQty, + }))} filters={ <> updateCriteria("topItems", (c) => ({ ...c, rangeDays: v }))} /> - 顯示 + {t("show")} @@ -278,7 +286,7 @@ export default function DeliveryChartPage() { getOptionLabel={(opt) => [opt.itemCode, opt.itemName].filter(Boolean).join(" - ") || opt.itemCode} isOptionEqualToValue={(a, b) => a.itemCode === b.itemCode} renderInput={(params) => ( - + )} renderTags={(value, getTagProps) => value.map((option, index) => { @@ -310,7 +318,7 @@ export default function DeliveryChartPage() { plotOptions: { bar: { horizontal: true, barHeight: "70%" } }, dataLabels: { enabled: true }, }} - series={[{ name: "數量", data: chartData.topItems.map((i) => i.totalQty) }]} + series={[{ name: t("warehouse_qty"), data: chartData.topItems.map((i) => i.totalQty) }]} type="bar" width="100%" height={Math.max(320, chartData.topItems.length * 36)} @@ -319,15 +327,15 @@ export default function DeliveryChartPage() { ({ - 日期: r.date, - 員工: r.staffName, - 揀單數: r.orderCount, - 總揀貨款數: r.itemKindCount, - 總揀貨件數: r.itemQtyPicked, - 總分鐘: r.totalMinutes, + [L("日期", "Date")]: r.date, + [t("delivery_colStaff")]: r.staffName, + [t("delivery_colPickCount")]: r.orderCount, + [t("delivery_colItemKindCount")]: r.itemKindCount, + [L("總揀貨件數", "Qty picked")]: r.itemQtyPicked, + [t("delivery_colTotalMin")]: r.totalMinutes, }))} filters={ <> @@ -342,7 +350,7 @@ export default function DeliveryChartPage() { /> @@ -352,7 +360,7 @@ export default function DeliveryChartPage() { /> @@ -361,9 +369,9 @@ export default function DeliveryChartPage() { InputLabelProps={{ shrink: true }} /> - 倉別 + {t("delivery_store")} @@ -388,7 +396,7 @@ export default function DeliveryChartPage() { getOptionLabel={(opt) => [opt.staffNo, opt.name].filter(Boolean).join(" - ") || opt.staffNo} isOptionEqualToValue={(a, b) => a.staffNo === b.staffNo} renderInput={(params) => ( - + )} renderTags={(value, getTagProps) => value.map((option, index) => { @@ -412,13 +420,19 @@ export default function DeliveryChartPage() { ) : chartData.staffPerf.length === 0 ? ( - 此日期範圍內尚無完成之發貨單,或無揀貨人資料。請更換日期範圍或確認發貨單(DO)已由員工完成並有紀錄揀貨時間。 + {L( + "此日期範圍內尚無完成之發貨單,或無揀貨人資料。請更換日期範圍或確認發貨單(DO)已由員工完成並有紀錄揀貨時間。", + "No completed delivery orders in this date range, or no picker was recorded. Change the date range, or confirm the delivery order (DO) was completed by staff with pick times recorded." + )} ) : ( <> - 週期內每人揀單數、總揀貨款數、總揀貨件數及總耗時(首揀至完成) + {L( + "週期內每人揀單數、總揀貨款數、總揀貨件數及總耗時(首揀至完成)", + "Pick count, item kinds, qty picked, and total time (first pick to completion) per person for this period." + )} - 員工 - 揀單數 - 總揀貨款數 - 總揀貨件數 - 總分鐘 - 平均分鐘/單 + {t("delivery_colStaff")} + {t("delivery_colPickCount")} + {t("delivery_colItemKindCount")} + {L("總揀貨件數", "Qty picked")} + {t("delivery_colTotalMin")} + {t("delivery_colAvgMin")} {staffPerfByStaff.length === 0 ? ( - 無數據 + {t("noData")} ) : ( staffPerfByStaff.map((row) => ( @@ -466,7 +480,7 @@ export default function DeliveryChartPage() { - 每日按員工單數 + {t("delivery_dailyByStaff")} r.date))).sort(), }, - yaxis: { title: { text: "單數" } }, + yaxis: { title: { text: t("delivery_orderCount") } }, plotOptions: { bar: { columnWidth: "60%" } }, dataLabels: { enabled: false }, legend: { position: "top" }, diff --git a/src/app/(main)/chart/equipment/board/page.tsx b/src/app/(main)/chart/equipment/board/page.tsx index ca38ca2e..219b7c25 100644 --- a/src/app/(main)/chart/equipment/board/page.tsx +++ b/src/app/(main)/chart/equipment/board/page.tsx @@ -35,6 +35,8 @@ import { fetchEquipmentUsageBoard, type EquipmentUsageBoardRow } from "@/app/api import SafeApexCharts from "@/components/charts/SafeApexCharts"; import { CHART_BOARD_REFRESH_INTERVAL_SEC_OPTIONS } from "@/app/(main)/chart/chartBoardRefreshPrefs"; import { useChartBoardRefreshPrefs } from "@/app/(main)/chart/useChartBoardRefreshPrefs"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; const EQUIPMENT_CHART_MAX = 35; @@ -62,17 +64,19 @@ function formatCodeNameLine(code: string, name: string): string { return `${c} ${n}`; } -function formatUsageMinutes(m: number): string { +function formatUsageMinutes(m: number, L: (zh: string, en: string) => string): string { if (!Number.isFinite(m) || m <= 0) return "—"; const rounded = Math.round(m * 10) / 10; - if (rounded < 60) return `${rounded} 分`; + if (rounded < 60) return L(`${rounded} 分`, `${rounded} min`); const h = Math.floor(rounded / 60); const mm = Math.round((rounded % 60) * 10) / 10; - return mm > 0 ? `${h} 小時 ${mm} 分` : `${h} 小時`; + return mm > 0 ? L(`${h} 小時 ${mm} 分`, `${h} hr ${mm} min`) : L(`${h} 小時`, `${h} hr`); } export default function EquipmentUsageBoardPage() { const theme = useTheme(); + const { t } = useTranslation("chart"); + const L = useUiText(); /** Calendar day for API (local). Default: today. */ const [viewDate, setViewDate] = useState(() => dayjs().format("YYYY-MM-DD")); const [rows, setRows] = useState([]); @@ -188,13 +192,13 @@ export default function EquipmentUsageBoardPage() { }, }, colors: ["#1976d2", "#0288d1", "#0097a7", "#00838f", "#00695c", "#2e7d32", "#558b2f", "#827717"], - dataLabels: { enabled: true, formatter: (val: number) => (Number.isFinite(val) ? `${val} 分` : "") }, - xaxis: { categories: equipmentUsageChart.categories, title: { text: "分鐘" } }, + dataLabels: { enabled: true, formatter: (val: number) => (Number.isFinite(val) ? L(`${val} 分`, `${val} min`) : "") }, + xaxis: { categories: equipmentUsageChart.categories, title: { text: t("minutes") } }, yaxis: { labels: { maxWidth: 260 } }, legend: { show: false }, - tooltip: { y: { formatter: (val: number) => `${val} 分鐘` } }, + tooltip: { y: { formatter: (val: number) => t("minutesWithVal", { val }) } }, }), - [equipmentUsageChart.categories], + [equipmentUsageChart.categories, L, t], ); const displayRows = useMemo(() => { @@ -216,7 +220,9 @@ export default function EquipmentUsageBoardPage() { }, [displayRows]); const isToday = viewDate === dayjs().format("YYYY-MM-DD"); - const weekdayZh = ["日", "一", "二", "三", "四", "五", "六"][dayjs(viewDate).day()] ?? ""; + const weekdayIndex = dayjs(viewDate).day(); + const weekdayZh = ["日", "一", "二", "三", "四", "五", "六"][weekdayIndex] ?? ""; + const weekdayEn = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekdayIndex] ?? ""; return ( - 設備使用看板 + {t("pageTitle_equipmentBoard")} - 資料來源與工單編輯/工藝流程一致。上方使用時間(分鐘)為各設備當日明細加總(有起訖則相減;產線 Pass/無完工時間時用預設生產分鐘)。 - 點長條圖可篩選下方列表,再點同一項取消。 - 快捷鍵(不在輸入框內時):T{" "} - 今日、Y 昨日。 + {L("資料來源與", "Data matches")}{" "} + {L("工單編輯/工藝流程", "Job Order Edit / Process Flow")} + {L("一致。上方", ". ")} + {L("使用時間(分鐘)", "Usage time (minutes)")} + {L( + "為各設備當日明細加總(有起訖則相減;產線 Pass/無完工時間時用預設生產分鐘)。點", + " above is the sum of each equipment row for the day (subtract end from start; a line Pass or a missing end time uses the default production minutes). Click a" + )}{" "} + {L("長條圖", "bar")} + {L("可篩選下方列表,再點同一項取消。", " to filter the list below. Click the same bar again to clear.")} + {L("快捷鍵", "Shortcuts")} + {L("(不在輸入框內時):", " (when the cursor is not in a text field): ")} + T{" "} + {t("today")} + {L("、", ", ")} + Y {t("yesterday")} + {L("。", ".")} {autoRefreshOn - ? ` 已開啟自動重新整理(每 ${refreshIntervalSec} 秒)` - : " 預設不自動更新,可開啟「自動重新整理」並選擇間隔。"} - {" 設定自動儲存在本機(登入時依帳號;未登入則為此分頁工作階段)。"} - {lastUpdated ? ` · 最後更新 ${lastUpdated}` : ""} + ? L( + ` 已開啟自動重新整理(每 ${refreshIntervalSec} 秒)`, + ` Auto refresh is on (every ${refreshIntervalSec} sec).` + ) + : L( + " 預設不自動更新,可開啟「自動重新整理」並選擇間隔。", + " Auto refresh is off by default. Turn on Auto Refresh and choose an interval." + )} + {L( + " 設定自動儲存在本機(登入時依帳號;未登入則為此分頁工作階段)。", + " Settings are saved on this device (per account when signed in, or for this browser session when signed out)." + )} + {lastUpdated ? L(` · 最後更新 ${lastUpdated}`, ` · Last updated ${lastUpdated}`) : ""} {error && ( @@ -271,31 +299,31 @@ export default function EquipmentUsageBoardPage() { color="text.secondary" sx={{ display: "block", mb: 1, lineHeight: 1.4, letterSpacing: 0.6 }} > - 查詢與列表 + {t("equipment_searchAndList")} - 歸屬日 {viewDate}(週{weekdayZh}) - {!isToday && } + {L(`歸屬日 ${viewDate}(週${weekdayZh})`, `Usage date ${viewDate} (${weekdayEn})`)} + {!isToday && } - + - + setViewDate(e.target.value)} @@ -303,7 +331,7 @@ export default function EquipmentUsageBoardPage() { sx={{ minWidth: 178 }} /> @@ -325,17 +353,17 @@ export default function EquipmentUsageBoardPage() { color="text.secondary" sx={{ display: "block", mb: 1, lineHeight: 1.4, letterSpacing: 0.6 }} > - 其他看板 + {t("otherBoards")} @@ -356,7 +384,7 @@ export default function EquipmentUsageBoardPage() { color="text.secondary" sx={{ display: "block", mb: 1, lineHeight: 1.4, letterSpacing: 0.6 }} > - 自動重新整理 + {t("autoRefresh")} setAutoRefreshOn(v)} - inputProps={{ "aria-label": "自動重新整理" }} + inputProps={{ "aria-label": t("autoRefresh") }} /> } - label="開啟" + label={t("on")} sx={{ ml: 0, mr: 0 }} /> - 間隔(秒) + {t("intervalSeconds")} @@ -394,7 +422,7 @@ export default function EquipmentUsageBoardPage() { setSelectedEquipmentKey(null)} color="primary" variant="outlined" @@ -405,15 +433,18 @@ export default function EquipmentUsageBoardPage() { {!loading && rows.length > 0 && equipmentUsageChart.data.length > 0 && ( - 使用時間(分鐘)— {viewDate} + {L(`使用時間(分鐘)— ${viewDate}`, `Usage time (minutes) — ${viewDate}`)} - 點擊長條篩選下方明細(最多顯示 {EQUIPMENT_CHART_MAX} 台,依分鐘數由高到低)。 + {L( + `點擊長條篩選下方明細(最多顯示 ${EQUIPMENT_CHART_MAX} 台,依分鐘數由高到低)。`, + `Click a bar to filter the rows below (up to ${EQUIPMENT_CHART_MAX} equipment, highest minutes first).` + )} @@ -422,7 +453,10 @@ export default function EquipmentUsageBoardPage() { {!loading && rows.length > 0 && equipmentUsageChart.data.length === 0 && ( - 當日有明細但無法加總使用分鐘(多數為缺開/完工時間且無預設生產分鐘)。仍可在下方表格檢視。 + {L( + "當日有明細但無法加總使用分鐘(多數為缺開/完工時間且無預設生產分鐘)。仍可在下方表格檢視。", + "This date has rows, but usage minutes could not be totaled (usually a missing start or end time and no default production minutes). You can still review them in the table below." + )} )} @@ -430,26 +464,26 @@ export default function EquipmentUsageBoardPage() { - {selectedEquipmentKey ? "篩選後筆數" : "該日總筆數"} + {selectedEquipmentKey ? L("篩選後筆數", "Filtered rows") : L("該日總筆數", "Rows for this date")} {stats.sessions} - 使用分鐘合計(篩選範圍) + {L("使用分鐘合計(篩選範圍)", "Total usage minutes (current filter)")} - {formatUsageMinutes(stats.totalMins)} + {formatUsageMinutes(stats.totalMins, L)} - 涉及設備數(篩選範圍) + {L("涉及設備數(篩選範圍)", "Equipment count (current filter)")} {stats.equipmentTouched} {stats.working > 0 && ( - 設備工時未結案 + {t("equipment_unclosedHours")} {stats.working} @@ -464,16 +498,19 @@ export default function EquipmentUsageBoardPage() { ) : rows.length === 0 ? ( - 此日期沒有符合歸屬日的設備使用紀錄(含工藝流程明細),或該日尚無已完工且已填設備的步驟。 + {L( + "此日期沒有符合歸屬日的設備使用紀錄(含工藝流程明細),或該日尚無已完工且已填設備的步驟。", + "No equipment usage for this date (including process-flow rows), or no completed step with equipment filled in." + )} ) : displayRows.length === 0 ? ( - 此篩選下沒有明細。 + {L("此篩選下沒有明細。", "No rows match this filter.")} ) : ( @@ -481,16 +518,16 @@ export default function EquipmentUsageBoardPage() { - 狀態 - 設備 - 使用(分) - 工單 - 工序 - 工單計劃開始 - 開工時間 - 完工時間 - 操作員 - 開啟 + {t("equipment_status")} + {t("equipment_equipment")} + {L("使用(分)", "Usage (min)")} + {t("equipment_jobOrder")} + {t("equipment_process")} + {t("equipment_planStart")} + {t("equipment_startTime")} + {t("equipment_endTime")} + {t("equipment_operator")} + {L("開啟", "Open")} @@ -498,15 +535,15 @@ export default function EquipmentUsageBoardPage() { {r.workingNow === 1 ? ( - + ) : !r.operatingStart?.trim() && !r.operatingEnd?.trim() ? ( - + ) : ( - + )} {formatCodeNameLine(r.equipmentCode, r.equipmentName)} - {formatUsageMinutes(r.usageMinutes)} + {formatUsageMinutes(r.usageMinutes, L)} {r.jobOrderCode || "—"} {formatCodeNameLine(r.processCode, r.processName)} {r.jobPlanStart || "—"} @@ -521,7 +558,7 @@ export default function EquipmentUsageBoardPage() { rel="noopener noreferrer" size="small" > - 開啟 + {L("開啟", "Open")} diff --git a/src/app/(main)/chart/forecast/page.tsx b/src/app/(main)/chart/forecast/page.tsx index 537e3d11..f99537dc 100644 --- a/src/app/(main)/chart/forecast/page.tsx +++ b/src/app/(main)/chart/forecast/page.tsx @@ -18,13 +18,13 @@ import { fetchProductionScheduleByDate, fetchPlannedOutputByDateAndItem, } from "@/app/api/chart/client"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import ChartCard from "../_components/ChartCard"; import DateRangeSelect from "../_components/DateRangeSelect"; import { toDateRange, DEFAULT_RANGE_DAYS } from "../_components/constants"; import SafeApexCharts from "@/components/charts/SafeApexCharts"; -const PAGE_TITLE = "預測與計劃"; - const DISTINCT_ITEM_COLORS = [ "#d60000", "#018700", @@ -119,6 +119,8 @@ const defaultCriteria: Criteria = { }; export default function ForecastChartPage() { + const { t } = useTranslation("chart"); + const L = useUiText(); const [criteria, setCriteria] = useState(defaultCriteria); const [error, setError] = useState(null); const [chartData, setChartData] = useState<{ @@ -201,7 +203,7 @@ export default function ForecastChartPage() { return ( - {PAGE_TITLE} + {t("pageTitle_forecast")} {error && ( setError(null)}> @@ -210,9 +212,14 @@ export default function ForecastChartPage() { )} ({ 日期: r.date, 物料編碼: r.itemCode, 物料名稱: r.itemName, 數量: r.qty }))} + title={L("按物料計劃日產量(預測)", "Planned Daily Output by Item (Forecast)")} + exportFilename={L("按物料計劃日產量_預測", "Planned_Daily_Output_By_Item")} + exportData={filteredPlannedOutputRows.map((r) => ({ + [L("日期", "Date")]: r.date, + [L("物料編碼", "Item Code")]: r.itemCode, + [L("物料名稱", "Item Name")]: r.itemName, + [t("warehouse_qty")]: r.qty, + }))} filters={ updateCriteria("plannedOutputByDate", (c) => ({ ...c, rangeDays: v }))} /> - 物料編碼 + {L("物料編碼", "Item Code")} setRefreshIntervalSec(Number(e.target.value))} > {CHART_BOARD_REFRESH_INTERVAL_SEC_OPTIONS.map((sec) => ( - {sec} 秒 + {L(`${sec} 秒`, `${sec} sec`)} ))} @@ -730,10 +771,15 @@ export default function JobOrderBoardPage() { - 工單狀態分佈 + {L("工單狀態分佈", "Job Order Status")} - 點擊圓環或下方圖例可依階段篩選列表;再點同一項或表格上方「清除列表篩選」可還原。 + {L("點擊圓環或下方圖例可依", "Click a slice or the legend to filter the list by")}{" "} + {L("階段", "stage")} + {L( + "篩選列表;再點同一項或表格上方「清除列表篩選」可還原。", + ". Click the same item again, or Clear list filter above the table, to reset." + )} {statusDonut.series.length > 0 ? ( ) : ( - 無資料 + {L("無資料", "No data")} )} - 本頁摘要 + {L("本頁摘要", "Summary on this page")} - 領料:待領 {materialSummary.pending} / 已揀 {materialSummary.picked} + {L( + `領料:待領 ${materialSummary.pending} / 已揀 ${materialSummary.picked}`, + `Material: pending ${materialSummary.pending} / picked ${materialSummary.picked}` + )} - 成品/半成品入庫(驗收數量)— QC 中/已驗待入/已入庫 + {L( + "成品/半成品入庫(驗收數量)— QC 中/已驗待入/已入庫", + "Finished goods / WIP stock-in (accepted qty) — in QC / awaiting put-away / stocked" + )} - 點擊長條色塊或上方圖例,篩選該入庫區塊有資料的工單;再點一次可還原。 + {L( + "點擊長條色塊或上方圖例,篩選該入庫區塊有資料的工單;再點一次可還原。", + "Click a bar segment or the legend to show job orders with qty in that stock-in bucket. Click again to reset." + )} {fgTotals.qc + fgTotals.ready + fgTotals.stocked > 0 ? ( @@ -768,9 +823,9 @@ export default function JobOrderBoardPage() { chartRevision={`fg-bar-${fgTotals.qc}-${fgTotals.ready}-${fgTotals.stocked}`} options={fgSummaryChartOptions} series={[ - { name: "QC 中", data: [Math.max(0, fgTotals.qc)] }, - { name: "已驗待入", data: [Math.max(0, fgTotals.ready)] }, - { name: "已入庫", data: [Math.max(0, fgTotals.stocked)] }, + { name: L("QC 中", "In QC"), data: [Math.max(0, fgTotals.qc)] }, + { name: L("已驗待入", "Awaiting put-away"), data: [Math.max(0, fgTotals.ready)] }, + { name: L("已入庫", "Stocked"), data: [Math.max(0, fgTotals.stocked)] }, ]} type="bar" height={140} @@ -784,13 +839,15 @@ export default function JobOrderBoardPage() { setTableStatusKey((p) => (p === "processing" ? null : "processing")); }} > - {tableStatusKey === "processing" ? "取消「製程中」篩選" : "篩選製程中工單(階段)"} + {tableStatusKey === "processing" + ? L("取消「製程中」篩選", "Clear “In Process” filter") + : L("篩選製程中工單(階段)", "Filter job orders in process (stage)")} ) : ( <> - 驗收數量合計為 0,不顯示長條圖 + {L("驗收數量合計為 0,不顯示長條圖", "Accepted qty totals 0, so the bar chart is hidden.")} )} @@ -815,7 +874,7 @@ export default function JobOrderBoardPage() { ) : rows.length === 0 ? ( - 此條件下沒有工單 + {L("此條件下沒有工單", "No job orders for this filter.")} ) : ( @@ -823,11 +882,14 @@ export default function JobOrderBoardPage() { - 列表篩選中: + {L("列表篩選中:", "List filter:")} {tableStockBucket && ( - 清除列表篩選 + {L("清除列表篩選", "Clear list filter")} @@ -860,13 +922,13 @@ export default function JobOrderBoardPage() { - 工單號 - 階段 - 領料進度 - 工序進度 - 當前製程 - 成品/半成品入庫 - 開啟 + {L("工單號", "Job Order No.")} + {L("階段", "Stage")} + {L("領料進度", "Pick progress")} + {L("工序進度", "Process progress")} + {L("當前製程", "Current process")} + {L("成品/半成品入庫", "FG / WIP stock-in")} + {L("開啟", "Open")} @@ -875,10 +937,16 @@ export default function JobOrderBoardPage() { {tableStockBucket - ? "目前沒有符合此入庫區塊的工單(可點「清除列表篩選」)。" + ? L( + "目前沒有符合此入庫區塊的工單(可點「清除列表篩選」)。", + "No job orders match this stock-in bucket. Click Clear list filter." + ) : tableStatusKey - ? "此階段下目前沒有工單(可點「清除列表篩選」)。" - : "沒有資料"} + ? L( + "此階段下目前沒有工單(可點「清除列表篩選」)。", + "No job orders in this stage. Click Clear list filter." + ) + : L("沒有資料", "No data")} @@ -901,7 +969,7 @@ export default function JobOrderBoardPage() { - {statusLabelZh(r.status)} + {statusLabel(r.status, L)} @@ -921,7 +989,9 @@ export default function JobOrderBoardPage() { - 已驗待入 {r.fgReadyToStockInCount} 行,驗收數量 {formatQty(r.fgReadyToStockInQty)};全單驗收合計{" "} + {L("已驗待入", "Awaiting put-away")} {r.fgReadyToStockInCount}{" "} + {L("行,驗收數量", "lines, accepted qty")} {formatQty(r.fgReadyToStockInQty)} + {L(";全單驗收合計", "; order accepted total")}{" "} {formatQty(r.stockInAcceptedQtyTotal)} @@ -933,7 +1003,7 @@ export default function JobOrderBoardPage() { rel="noopener noreferrer" size="small" > - 開啟 + {L("開啟", "Open")} @@ -942,11 +1012,14 @@ export default function JobOrderBoardPage() { - 明細 + {L("明細", "Details")} - 生產流程摘要(與工單·工藝流程一致,來自產線彙總) + {L( + "生產流程摘要(與工單·工藝流程一致,來自產線彙總)", + "Production summary (same as the job order process flow, from the line rollup)" + )} - {r.jobTypeName?.trim() ? r.jobTypeName : "—"} - + {r.jobTypeName?.trim() ? r.jobTypeName : "—"} + {formatQty(r.reqQty)} - {r.outputQtyUom ? `(${r.outputQtyUom})` : ""} + {r.outputQtyUom ? L(`(${r.outputQtyUom})`, ` (${r.outputQtyUom})`) : ""} + + {r.productionDate || "—"} + + {formatDurationMins(r.actualLineMinsTotal, L)} - {r.productionDate || "—"} - {formatDurationMins(r.actualLineMinsTotal)} - - {r.planProcessingMinsTotal > 0 ? `${Math.round(r.planProcessingMinsTotal)} 分鐘` : "—"} + + {r.planProcessingMinsTotal > 0 + ? L( + `${Math.round(r.planProcessingMinsTotal)} 分鐘`, + `${Math.round(r.planProcessingMinsTotal)} min` + ) + : "—"} {r.planSetupChangeoverMinsTotal > 0 - ? ` · 備料/轉換合計 ${Math.round(r.planSetupChangeoverMinsTotal)} 分鐘` + ? L( + ` · 備料/轉換合計 ${Math.round(r.planSetupChangeoverMinsTotal)} 分鐘`, + ` · Setup and changeover ${Math.round(r.planSetupChangeoverMinsTotal)} min` + ) : ""} - {r.productProcessStart || "—"} - + {r.productProcessStart || "—"} + {formatAssumeEndMmDdHhMm(r.productProcessStart, r.planProcessingMinsTotal)} - 工單計劃/實際 + {L("工單計劃/實際", "Job Order Plan / Actual")} - {r.planStart || "—"} - {r.actualStart || "—"} - {r.planEnd || "—"} - {r.actualEnd || "—"} - {r.currentProcessStartTime || "—"} - + {r.planStart || "—"} + {r.actualStart || "—"} + {r.planEnd || "—"} + {r.actualEnd || "—"} + {r.currentProcessStartTime || "—"} + - {statusLabelZh(r.status)} + {statusLabel(r.status, L)} - 成品/半成品入庫狀況 + {L("成品/半成品入庫狀況", "FG / WIP stock-in")} - - {formatQty(r.fgInQcQty)}({r.fgInQcLineCount} 行) + + {formatQty(r.fgInQcQty)} + {L(`(${r.fgInQcLineCount} 行)`, ` (${r.fgInQcLineCount} lines)`)} - - {formatQty(r.fgReadyToStockInQty)}({r.fgReadyToStockInCount} 行) + + {formatQty(r.fgReadyToStockInQty)} + {L(`(${r.fgReadyToStockInCount} 行)`, ` (${r.fgReadyToStockInCount} lines)`)} - {formatQty(r.fgStockedQty)} - {formatQty(r.stockInAcceptedQtyTotal)} + {formatQty(r.fgStockedQty)} + {formatQty(r.stockInAcceptedQtyTotal)} diff --git a/src/app/(main)/chart/joborder/page.tsx b/src/app/(main)/chart/joborder/page.tsx index 15ecc369..e5e82dd5 100644 --- a/src/app/(main)/chart/joborder/page.tsx +++ b/src/app/(main)/chart/joborder/page.tsx @@ -14,13 +14,13 @@ import { fetchJobProcessPendingCompletedByDate, fetchJobEquipmentWorkingWorkedByDate, } from "@/app/api/chart/client"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import ChartCard from "../_components/ChartCard"; import DateRangeSelect from "../_components/DateRangeSelect"; import { toDateRange, DEFAULT_RANGE_DAYS } from "../_components/constants"; import SafeApexCharts from "@/components/charts/SafeApexCharts"; -const PAGE_TITLE = "工單"; - type Criteria = { joCountByDate: { rangeDays: number }; joCreatedCompleted: { rangeDays: number }; @@ -34,6 +34,8 @@ const defaultCriteria: Criteria = { }; export default function JobOrderChartPage() { + const { t } = useTranslation("chart"); + const L = useUiText(); const [joTargetDate, setJoTargetDate] = useState(() => dayjs().format("YYYY-MM-DD")); const [criteria, setCriteria] = useState(defaultCriteria); const [error, setError] = useState(null); @@ -155,11 +157,11 @@ export default function JobOrderChartPage() { - {PAGE_TITLE} + {t("pageTitle_jobOrder")} @@ -182,13 +184,16 @@ export default function JobOrderChartPage() { )} ({ 狀態: p.status, 數量: p.count }))} + title={t("jo_byStatus")} + exportFilename={t("jo_byStatus_export")} + exportData={chartData.joStatus.map((p) => ({ + [L("狀態", "Status")]: p.status, + [t("warehouse_qty")]: p.count, + }))} filters={ setJoTargetDate(e.target.value)} @@ -215,9 +220,12 @@ export default function JobOrderChartPage() { ({ 日期: d.date, 工單數: d.orderCount }))} + title={L("按日期工單數量(計劃開始日)", "Job Order Count by Date (Plan Start)")} + exportFilename={L("工單數量_按日期", "Job_Order_Count_By_Date")} + exportData={chartData.joCountByDate.map((d) => ({ + [L("日期", "Date")]: d.date, + [L("工單數", "Job Order Count")]: d.orderCount, + }))} filters={ d.date) }, - yaxis: { title: { text: "單數" } }, + yaxis: { title: { text: t("delivery_orderCount") } }, plotOptions: { bar: { columnWidth: "60%" } }, dataLabels: { enabled: false }, }} - series={[{ name: "工單數", data: chartData.joCountByDate.map((d) => d.orderCount) }]} + series={[{ name: L("工單數", "Job Order Count"), data: chartData.joCountByDate.map((d) => d.orderCount) }]} type="bar" width="100%" height={320} @@ -245,9 +253,13 @@ export default function JobOrderChartPage() { ({ 日期: d.date, 創建: d.createdCount, 完成: d.completedCount }))} + title={t("jo_createdVsCompleted")} + exportFilename={L("工單創建與完成_按日期", "Job_Orders_Created_vs_Completed")} + exportData={chartData.joCreatedCompleted.map((d) => ({ + [L("日期", "Date")]: d.date, + [L("創建", "Created")]: d.createdCount, + [t("series_completed")]: d.completedCount, + }))} filters={ d.date) }, - yaxis: { title: { text: "數量" } }, + yaxis: { title: { text: t("warehouse_qty") } }, stroke: { curve: "smooth" }, dataLabels: { enabled: false }, }} series={[ - { name: "創建", data: chartData.joCreatedCompleted.map((d) => d.createdCount) }, - { name: "完成", data: chartData.joCreatedCompleted.map((d) => d.completedCount) }, + { name: L("創建", "Created"), data: chartData.joCreatedCompleted.map((d) => d.createdCount) }, + { name: t("series_completed"), data: chartData.joCreatedCompleted.map((d) => d.completedCount) }, ]} type="line" width="100%" @@ -278,12 +290,16 @@ export default function JobOrderChartPage() { - 工單物料/工序/設備 + {t("jo_detailSection")} ({ 日期: d.date, 待領: d.pendingCount, 已揀: d.pickedCount }))} + title={t("jo_materialPendingPicked")} + exportFilename={L("工單物料_待領已揀_按日期", "Job_Material_Pending_vs_Picked")} + exportData={chartData.joMaterial.map((d) => ({ + [L("日期", "Date")]: d.date, + [L("待領", "Pending")]: d.pendingCount, + [L("已揀", "Picked")]: d.pickedCount, + }))} filters={ d.date) }, - yaxis: { title: { text: "筆數" } }, + yaxis: { title: { text: L("筆數", "Count") } }, plotOptions: { bar: { columnWidth: "60%" } }, dataLabels: { enabled: false }, legend: { position: "top" }, }} series={[ - { name: "待領", data: chartData.joMaterial.map((d) => d.pendingCount) }, - { name: "已揀", data: chartData.joMaterial.map((d) => d.pickedCount) }, + { name: L("待領", "Pending"), data: chartData.joMaterial.map((d) => d.pendingCount) }, + { name: L("已揀", "Picked"), data: chartData.joMaterial.map((d) => d.pickedCount) }, ]} type="bar" width="100%" @@ -315,9 +331,13 @@ export default function JobOrderChartPage() { ({ 日期: d.date, 待完成: d.pendingCount, 已完成: d.completedCount }))} + title={t("jo_processPendingCompleted")} + exportFilename={L("工單工序_待完成已完成_按日期", "Job_Process_Pending_vs_Completed")} + exportData={chartData.joProcess.map((d) => ({ + [L("日期", "Date")]: d.date, + [L("待完成", "Pending")]: d.pendingCount, + [L("已完成", "Completed")]: d.completedCount, + }))} filters={ d.date) }, - yaxis: { title: { text: "筆數" } }, + yaxis: { title: { text: L("筆數", "Count") } }, plotOptions: { bar: { columnWidth: "60%" } }, dataLabels: { enabled: false }, legend: { position: "top" }, }} series={[ - { name: "待完成", data: chartData.joProcess.map((d) => d.pendingCount) }, - { name: "已完成", data: chartData.joProcess.map((d) => d.completedCount) }, + { name: L("待完成", "Pending"), data: chartData.joProcess.map((d) => d.pendingCount) }, + { name: L("已完成", "Completed"), data: chartData.joProcess.map((d) => d.completedCount) }, ]} type="bar" width="100%" @@ -349,9 +369,13 @@ export default function JobOrderChartPage() { ({ 日期: d.date, 使用中: d.workingCount, 已使用: d.workedCount }))} + title={t("jo_equipmentWorkingWorked")} + exportFilename={L("工單設備_使用中已使用_按日期", "Job_Equipment_In_Use_vs_Used")} + exportData={chartData.joEquipment.map((d) => ({ + [L("日期", "Date")]: d.date, + [L("使用中", "In Use")]: d.workingCount, + [L("已使用", "Used")]: d.workedCount, + }))} filters={ d.date) }, - yaxis: { title: { text: "筆數" } }, + yaxis: { title: { text: L("筆數", "Count") } }, plotOptions: { bar: { columnWidth: "60%" } }, dataLabels: { enabled: false }, legend: { position: "top" }, }} series={[ - { name: "使用中", data: chartData.joEquipment.map((d) => d.workingCount) }, - { name: "已使用", data: chartData.joEquipment.map((d) => d.workedCount) }, + { name: L("使用中", "In Use"), data: chartData.joEquipment.map((d) => d.workingCount) }, + { name: L("已使用", "Used"), data: chartData.joEquipment.map((d) => d.workedCount) }, ]} type="bar" width="100%" diff --git a/src/app/(main)/chart/process/board/page.tsx b/src/app/(main)/chart/process/board/page.tsx index ab8adb52..2ccbaf6b 100644 --- a/src/app/(main)/chart/process/board/page.tsx +++ b/src/app/(main)/chart/process/board/page.tsx @@ -42,12 +42,18 @@ import { fetchProcessBoard, type ProcessBoardRow } from "@/app/api/chart/client" import SafeApexCharts from "@/components/charts/SafeApexCharts"; import { CHART_BOARD_REFRESH_INTERVAL_SEC_OPTIONS } from "@/app/(main)/chart/chartBoardRefreshPrefs"; import { useChartBoardRefreshPrefs } from "@/app/(main)/chart/useChartBoardRefreshPrefs"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; -const BOARD_STATUS_ZH: Record = { - pending: "未開工", - in_progress: "進行中", - completed: "已完工", -}; +type UiText = (zh: string, en: string) => string; + +function boardStatusLabel(status: string, t: (key: string) => string): string { + const k = norm(status); + if (k === "pending") return t("process_notStarted"); + if (k === "in_progress") return t("process_inProgress"); + if (k === "completed") return t("process_completed"); + return status; +} const BOARD_STATUS_ORDER = ["in_progress", "pending", "completed"] as const; @@ -161,8 +167,9 @@ function processAccentColor(processId: number, barColorMap: Map) } function BoardStatusVisual({ status }: { status: string }) { + const { t } = useTranslation("chart"); const k = norm(status); - const label = BOARD_STATUS_ZH[k] ?? status; + const label = boardStatusLabel(status, t); if (k === "completed") { return ( (x ?? "").trim()).join("-") || "—"; const assumeEnd = formatAssumeEndMmDdHhMm(r.productProcessStart, r.planProcessingMinsTotal); @@ -263,28 +271,28 @@ function ProcessBoardJoSummary({ r }: { r: ProcessBoardRow }) { - 工單類型{" "} + {L("工單類型", "Job Order Type")}{" "} {r.jobTypeName?.trim() || "—"} - 數量{" "} + {L("數量", "Qty")}{" "} {formatQtyBoard(r.reqQty)} - {r.outputQtyUom ? `(${r.outputQtyUom})` : ""} + {r.outputQtyUom ? L(`(${r.outputQtyUom})`, ` (${r.outputQtyUom})`) : ""} - 生產日期{" "} + {L("生產日期", "Production date")}{" "} {r.productionDate || "—"} - 實際耗時(全單線段加總){" "} + {L("實際耗時(全單線段加總)", "Actual time (sum of all line segments)")}{" "} - {formatDurationMins(r.actualLineMinsTotal)} + {formatDurationMins(r.actualLineMinsTotal, L)} - 預計生產{" "} + {L("預計生產", "Planned production")}{" "} - {r.planProcessingMinsTotal > 0 ? `${Math.round(r.planProcessingMinsTotal)} 分` : "—"} + {r.planProcessingMinsTotal > 0 + ? L(`${Math.round(r.planProcessingMinsTotal)} 分`, `${Math.round(r.planProcessingMinsTotal)} min`) + : "—"} {r.planSetupChangeoverMinsTotal > 0 - ? ` · 備轉 ${Math.round(r.planSetupChangeoverMinsTotal)} 分` + ? L( + ` · 備轉 ${Math.round(r.planSetupChangeoverMinsTotal)} 分`, + ` · Setup ${Math.round(r.planSetupChangeoverMinsTotal)} min` + ) : ""} - 開始{" "} + {L("開始", "Start")}{" "} {startMmDd} - 預計完成{" "} + {L("預計完成", "Estimated finish")}{" "} {assumeEnd} @@ -326,13 +339,14 @@ function ProcessBoardJoSummary({ r }: { r: ProcessBoardRow }) { } function StepPlanActualMins({ plan, actual }: { plan: number; actual: number }) { + const L = useUiText(); return ( - 預計 {plan > 0 ? Math.round(plan) : "—"} 分 + {L("預計", "Plan")} {plan > 0 ? Math.round(plan) : "—"} {L("分", "min")} - 實際 {formatDurationMins(actual)} + {L("實際", "Actual")} {formatDurationMins(actual, L)} ); @@ -469,6 +483,8 @@ type ProcessGroupBlock = { key: string; title: string; rows: ProcessBoardRow[] } export default function ProcessBoardPage() { const theme = useTheme(); + const { t } = useTranslation("chart"); + const L = useUiText(); const [targetDate, setTargetDate] = useState(() => dayjs().format("YYYY-MM-DD")); const [allIncompleteOpen, setAllIncompleteOpen] = useState(false); const [rows, setRows] = useState([]); @@ -556,10 +572,10 @@ export default function ProcessBoardPage() { const allKeys = [...keys, ...extra]; return { keys: allKeys, - labels: allKeys.map((k) => BOARD_STATUS_ZH[k] ?? k), + labels: allKeys.map((k) => boardStatusLabel(k, t)), series: allKeys.map((k) => map.get(k) ?? 0), }; - }, [rows]); + }, [rows, t]); const processBar = useMemo(() => { const m = new Map(); @@ -692,8 +708,8 @@ export default function ProcessBoardPage() { const filterProcessLabel = useMemo(() => { if (filterProcessId == null) return ""; const hit = rows.find((r) => r.processId === filterProcessId); - return hit ? formatCodeNameLine(hit.processCode, hit.processName) : `工序 #${filterProcessId}`; - }, [filterProcessId, rows]); + return hit ? formatCodeNameLine(hit.processCode, hit.processName) : L(`工序 #${filterProcessId}`, `Process #${filterProcessId}`); + }, [filterProcessId, rows, L]); const grouped = useMemo((): ProcessGroupBlock[] => { if (groupMode === "status") { @@ -708,7 +724,7 @@ export default function ProcessBoardPage() { }); return BOARD_STATUS_ORDER.filter((k) => (buckets[k]?.length ?? 0) > 0).map((k) => ({ key: k, - title: BOARD_STATUS_ZH[k] ?? k, + title: boardStatusLabel(k, t), rows: buckets[k] ?? [], })); } @@ -727,10 +743,12 @@ export default function ProcessBoardPage() { title: g.title, rows: g.rows.sort((a: ProcessBoardRow, b: ProcessBoardRow) => a.seqNo - b.seqNo), })); - }, [displayRows, groupMode]); + }, [displayRows, groupMode, t]); const isTargetToday = targetDate === dayjs().format("YYYY-MM-DD"); - const targetWeekdayZh = ["日", "一", "二", "三", "四", "五", "六"][dayjs(targetDate).day()] ?? ""; + const weekdayIndex = dayjs(targetDate).day(); + const targetWeekdayZh = ["日", "一", "二", "三", "四", "五", "六"][weekdayIndex] ?? ""; + const targetWeekdayEn = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekdayIndex] ?? ""; return ( - 工序即時看板 + {t("pageTitle_processBoard")} - 每列為一筆工單工序(job_order_process)。狀態與工單「工藝流程」一致:優先依產線明細(productprocessline)狀態與起訖時間;若無對應明細則沿用工序上的開工/完工時間。 + {L("每列為一筆工單工序(", "Each row is one job order process (")} + job_order_process + {L( + ")。狀態與工單「工藝流程」一致:優先依產線明細(", + "). Status matches the job order process flow: the line detail (" + )} + productprocessline + {L( + ")狀態與起訖時間;若無對應明細則沿用工序上的開工/完工時間。", + ") status and start/end times are used first. If there is no matching line, the process start and end times are used." + )} {autoRefreshOn - ? ` 已開啟自動重新整理(每 ${refreshIntervalSec} 秒)` - : " 預設不自動更新,可開啟「自動重新整理」並選擇間隔。"} - {" 設定自動儲存在本機(登入時依帳號;未登入則為此分頁工作階段)。"} - {" · 最後更新 "} + ? L( + ` 已開啟自動重新整理(每 ${refreshIntervalSec} 秒)`, + ` Auto refresh is on (every ${refreshIntervalSec} sec).` + ) + : L( + " 預設不自動更新,可開啟「自動重新整理」並選擇間隔。", + " Auto refresh is off by default. Turn on Auto Refresh and choose an interval." + )} + {L( + " 設定自動儲存在本機(登入時依帳號;未登入則為此分頁工作階段)。", + " Settings are saved on this device (per account when signed in, or for this browser session when signed out)." + )} + {L(" · 最後更新 ", " · Last updated ")} {lastUpdated || "—"} - 快捷鍵(不在輸入框內時): + {L("快捷鍵", "Shortcuts")} + {L("(不在輸入框內時):", " (when the cursor is not in a text field): ")} T {" "} - 今日、 + {t("today")} + {L("、", ", ")} Y {" "} - 昨日(僅更新工單計劃開始日)。 + {L("昨日(僅更新工單計劃開始日)。", "yesterday (updates the job order plan start date only).")} {error && ( @@ -798,15 +837,18 @@ export default function ProcessBoardPage() { color="text.secondary" sx={{ display: "block", mb: 1, lineHeight: 1.4, letterSpacing: 0.6 }} > - 查詢與列表 + {t("equipment_searchAndList")} - 工單計劃開始日 {targetDate}(週{targetWeekdayZh}) - {!isTargetToday && } + {L( + `工單計劃開始日 ${targetDate}(週${targetWeekdayZh})`, + `Job Order Plan Start Date ${targetDate} (${targetWeekdayEn})` + )} + {!isTargetToday && } - + - + setTargetDate(e.target.value)} @@ -841,7 +883,7 @@ export default function ProcessBoardPage() { sx={{ minWidth: 178 }} /> - 依工序狀態 - 依工序 + {L("依工序狀態", "By process status")} + {L("依工序", "By process")} @@ -882,17 +924,17 @@ export default function ProcessBoardPage() { color="text.secondary" sx={{ display: "block", mb: 1, lineHeight: 1.4, letterSpacing: 0.6 }} > - 其他看板 + {t("otherBoards")} @@ -913,7 +955,7 @@ export default function ProcessBoardPage() { color="text.secondary" sx={{ display: "block", mb: 1, lineHeight: 1.4, letterSpacing: 0.6 }} > - 自動重新整理 + {t("autoRefresh")} setAutoRefreshOn(v)} - inputProps={{ "aria-label": "自動重新整理" }} + inputProps={{ "aria-label": t("autoRefresh") }} /> } - label="開啟" + label={t("on")} sx={{ ml: 0, mr: 0 }} /> - 間隔(秒) + {t("intervalSeconds")} @@ -950,20 +992,20 @@ export default function ProcessBoardPage() { {(filterBoardStatus || filterProcessId != null) && ( - 篩選中: + {L("篩選中:", "Filtered:")} {filterBoardStatus ? ( setFilterBoardStatus(null)} size="small" /> ) : null} {filterProcessId != null ? ( - setFilterProcessId(null)} size="small" /> + setFilterProcessId(null)} size="small" /> ) : null} )} @@ -987,7 +1029,7 @@ export default function ProcessBoardPage() { - 工序列總數 + {L("工序列總數", "Process rows")} {rows.length} @@ -1025,7 +1067,7 @@ export default function ProcessBoardPage() { - 篩選後 + {L("篩選後", "Filtered")} } sx={{ mb: 2 }}> - 圓環圖與長條圖是查詢篩選條件(不是另一份獨立統計):點某一扇區或長條後,下方表格只顯示符合該條件的工序列,與上方「篩選後」筆數一致。再點同一項可取消篩選;亦可用「清除篩選」。 + {L("圓環圖與長條圖是", "The donut and bar charts are")}{" "} + {L("查詢篩選條件", "query filters")} + {L( + "(不是另一份獨立統計):點某一扇區或長條後,下方表格只顯示符合該條件的工序列,與上方「篩選後」筆數一致。再點同一項可取消篩選;亦可用「清除篩選」。", + " (not a separate total). After you click a slice or a bar, the table below shows only matching process rows, and the count matches Filtered above. Click the same item again to clear, or use Clear filters." + )} - 依工序狀態(點扇區 → 篩選下方列表) + {L("依工序狀態(點扇區 → 篩選下方列表)", "By process status (click a slice to filter the list)")} - 與「篩選後」聯動:選「進行中」僅列出進行中的工序列。 + {L( + "與「篩選後」聯動:選「進行中」僅列出進行中的工序列。", + "Linked to Filtered: choosing In Progress lists only in-progress process rows." + )} {statusDonut.series.some((n) => n > 0) ? ( ) : ( - 無資料 + {L("無資料", "No data")} )} - 依工序名稱 · 筆數(點長條 → 篩選下方列表) + {L("依工序名稱 · 筆數(點長條 → 篩選下方列表)", "By process name · count (click a bar to filter the list)")} - 每色代表不同工序;點長條後表格僅保留該工序。 + {L( + "每色代表不同工序;點長條後表格僅保留該工序。", + "Each color is a different process. After you click a bar, the table keeps only that process." + )} {processBar.data.length > 0 ? ( ) : ( - 無資料 + {L("無資料", "No data")} )} @@ -1099,7 +1152,7 @@ export default function ProcessBoardPage() { ) : rows.length === 0 ? ( - 此條件下沒有工序資料。 + {L("此條件下沒有工序資料。", "No process data for this filter.")} ) : ( @@ -1143,13 +1196,19 @@ export default function ProcessBoardPage() { {g.title} - · {g.rows.length} 筆 + · {L(`${g.rows.length} 筆`, `${g.rows.length} rows`)} - 表格較寬時可左右捲動;上方同步捲軸與表格對齊,無需捲到頁面底部。點工單號於新分頁開啟編輯;序顯示於單號下方。 + {L("表格較寬時可", "When the table is wide, ")} + {L("左右捲動", "scroll sideways")} + {L(";上方", ". The ")} + {L("同步捲軸", "synced scrollbar")} + {L("與表格對齊,無需捲到頁面底部。", " above stays aligned with the table, so you do not need to scroll to the bottom of the page. ")} + {L("點工單號", "Click the job order no.")} + {L("於新分頁開啟編輯;序顯示於單號下方。", " to open edit in a new tab. Sequence is shown under the number.")}
- 工序狀態 + {L("工序狀態", "Process status")} - 工單 + {t("equipment_jobOrder")} - 點選開啟 · 序在下 + {L("點選開啟 · 序在下", "Click to open · sequence below")} - 生產流程摘要 + {L("生產流程摘要", "Production summary")} - 本工序時間 + {L("本工序時間", "This process time")} - 預計/實際 + {L("預計/實際", "Plan / actual")} - 名稱/描述 + {L("名稱/描述", "Name / description")} - 設備 + {t("equipment_equipment")} - 類型-名稱-編號 + {L("類型-名稱-編號", "Type-name-code")} - 員工 + {t("delivery_staff")} - 開工/完工 + {L("開工/完工", "Start / finish")} - 本工序 + {L("本工序", "This process")} @@ -1219,7 +1278,7 @@ export default function ProcessBoardPage() { - + - 序 + {L("序", "Seq")} - 開工 + {L("開工", "Start")} {timeCell(r.startTime)} - 完工 + {L("完工", "Finish")} {timeCell(r.endTime)} diff --git a/src/app/(main)/chart/purchase/exportPurchaseChartMaster.ts b/src/app/(main)/chart/purchase/exportPurchaseChartMaster.ts index 536e1f9f..beb59fe5 100644 --- a/src/app/(main)/chart/purchase/exportPurchaseChartMaster.ts +++ b/src/app/(main)/chart/purchase/exportPurchaseChartMaster.ts @@ -22,33 +22,78 @@ export type PurchaseChartMasterExportPayload = { purchaseOrderLineRows: Record[]; }; -function sheetOrPlaceholder(name: string, rows: Record[], emptyMessage: string): MultiSheetSpec { +type UiText = (zh: string, en: string) => string; + +const identityText: UiText = (zh) => zh; + +function sheetOrPlaceholder( + name: string, + rows: Record[], + emptyMessage: string, + noteHeader: string +): MultiSheetSpec { if (rows.length > 0) return { name, rows }; return { name, - rows: [{ 說明: emptyMessage }], + rows: [{ [noteHeader]: emptyMessage }], }; } /** * Build worksheet specs (used by {@link exportPurchaseChartMasterToFile}). + * `L` localizes sheet names and empty-sheet notes. Defaults keep Chinese. */ -export function buildPurchaseChartMasterSheets(payload: PurchaseChartMasterExportPayload): MultiSheetSpec[] { +export function buildPurchaseChartMasterSheets( + payload: PurchaseChartMasterExportPayload, + L: UiText = identityText +): MultiSheetSpec[] { + const note = L("說明", "Note"); return [ - { name: "篩選條件與情境", rows: payload.metaRows }, - sheetOrPlaceholder("預計送貨", payload.estimatedDonutRows, "無資料(請確認訂單日期與篩選)"), - sheetOrPlaceholder("實際已送貨", payload.actualStatusDonutRows, "無資料"), - sheetOrPlaceholder("貨品摘要", payload.itemSummaryRows, "無資料(可能為篩選交集為空或未載入)"), - sheetOrPlaceholder("供應商分佈", payload.supplierDistributionRows, "無資料"), - sheetOrPlaceholder("採購單列表", payload.purchaseOrderListRows, "無採購單明細可匯出"), - sheetOrPlaceholder("採購單行明細", payload.purchaseOrderLineRows, "無行資料(採購單列表為空)"), + { name: L("篩選條件與情境", "Filters and context"), rows: payload.metaRows }, + sheetOrPlaceholder( + L("預計送貨", "Estimated delivery"), + payload.estimatedDonutRows, + L("無資料(請確認訂單日期與篩選)", "No data (check the order date and filters)."), + note + ), + sheetOrPlaceholder( + L("實際已送貨", "Delivered"), + payload.actualStatusDonutRows, + L("無資料", "No data"), + note + ), + sheetOrPlaceholder( + L("貨品摘要", "Item summary"), + payload.itemSummaryRows, + L("無資料(可能為篩選交集為空或未載入)", "No data (the filter intersection may be empty, or data has not loaded)."), + note + ), + sheetOrPlaceholder( + L("供應商分佈", "Supplier distribution"), + payload.supplierDistributionRows, + L("無資料", "No data"), + note + ), + sheetOrPlaceholder( + L("採購單列表", "Purchase order list"), + payload.purchaseOrderListRows, + L("無採購單明細可匯出", "No purchase order lines to export."), + note + ), + sheetOrPlaceholder( + L("採購單行明細", "Purchase order line details"), + payload.purchaseOrderLineRows, + L("無行資料(採購單列表為空)", "No line data (the purchase order list is empty)."), + note + ), ]; } export function exportPurchaseChartMasterToFile( payload: PurchaseChartMasterExportPayload, - filenameBase: string + filenameBase: string, + L: UiText = identityText ): void { - const sheets = buildPurchaseChartMasterSheets(payload); + const sheets = buildPurchaseChartMasterSheets(payload, L); exportMultiSheetToXlsx(sheets, filenameBase); } diff --git a/src/app/(main)/chart/purchase/page.tsx b/src/app/(main)/chart/purchase/page.tsx index 075ae65a..7b54cc0c 100644 --- a/src/app/(main)/chart/purchase/page.tsx +++ b/src/app/(main)/chart/purchase/page.tsx @@ -42,9 +42,10 @@ import { import ChartCard from "../_components/ChartCard"; import { exportPurchaseChartMasterToFile } from "./exportPurchaseChartMaster"; import dayjs from "dayjs"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import SafeApexCharts from "@/components/charts/SafeApexCharts"; -const PAGE_TITLE = "採購"; const DEFAULT_DRILL_STATUS = "completed"; /** Must match backend `getPurchaseOrderByStatus` (orderDate). Using "complete" here desyncs drill-down from the donut counts. */ const DRILL_DATE_FILTER = "order" as const; @@ -63,30 +64,32 @@ function deferChartClick(fn: () => void) { } /** UI labels only; API still uses English status values. */ -function poStatusLabelZh(status: string): string { +type UiText = (zh: string, en: string) => string; + +function poStatusLabel(status: string, L: UiText): string { const s = status.trim().toLowerCase(); switch (s) { case "pending": - return "待處理"; + return L("待處理", "Pending"); case "completed": - return "已完成"; + return L("已完成", "Completed"); case "receiving": - return "收貨中"; + return L("收貨中", "Receiving"); default: return status; } } -function bucketLabelZh(bucket: string): string { +function bucketLabel(bucket: string, L: UiText): string { switch (bucket) { case "delivered": - return "已送"; + return L("已送", "Delivered"); case "not_delivered": - return "未送"; + return L("未送", "Not Delivered"); case "cancelled": - return "已取消"; + return L("已取消", "Cancelled"); case "other": - return "其他"; + return L("其他", "Other"); default: return bucket; } @@ -97,6 +100,8 @@ function emptyFilterOptions(): PurchaseOrderFilterOptions { } export default function PurchaseChartPage() { + const { t } = useTranslation("chart"); + const L = useUiText(); const [poTargetDate, setPoTargetDate] = useState(() => dayjs().format("YYYY-MM-DD")); const [error, setError] = useState(null); const [chartData, setChartData] = useState<{ status: string; count: number }[]>([]); @@ -405,98 +410,117 @@ export default function PurchaseChartPage() { setMasterExportLoading(true); try { const exportedAtIso = new Date().toISOString(); + const sep = L(";", "; "); + const none = L("(未選)", "(None selected)"); + const field = L("項目", "Field"); + const valueCol = L("值", "Value"); + const meta = (labelZh: string, labelEn: string, value: unknown) => ({ + [field]: L(labelZh, labelEn), + [valueCol]: value, + }); const filterSupplierText = filterOptions.suppliers .filter((s) => filterSupplierIds.includes(s.supplierId)) .map((s) => `${s.code} ${s.name}`.trim()) - .join(";") || "(未選)"; + .join(sep) || none; const filterItemText = filterOptions.items .filter((i) => filterItemCodes.includes(i.itemCode)) .map((i) => `${i.itemCode} ${i.itemName}`.trim()) - .join(";") || "(未選)"; - const filterPoText = filterPoNos.length ? filterPoNos.join(";") : "(未選)"; + .join(sep) || none; + const filterPoText = filterPoNos.length ? filterPoNos.join(sep) : none; + const defaultStatusLabel = poStatusLabel(DEFAULT_DRILL_STATUS, L); const metaRows: Record[] = [ - { 項目: "匯出時間_UTC", 值: exportedAtIso }, - { 項目: "訂單日期", 值: poTargetDate }, - { 項目: "多選_供應商", 值: filterSupplierText }, - { 項目: "多選_貨品", 值: filterItemText }, - { 項目: "多選_採購單號", 值: filterPoText }, - { - 項目: "預計送貨圓環_點選", - 值: selectedEstimatedBucket ? bucketLabelZh(selectedEstimatedBucket) : "(未選)", - }, - { - 項目: "實際已送貨圓環_點選狀態", - 值: - selectedStatus != null - ? poStatusLabelZh(selectedStatus) - : `(未選;下方圖表預設狀態 ${poStatusLabelZh(DEFAULT_DRILL_STATUS)})`, - }, - { 項目: "下方圖表套用狀態_英文", 值: effectiveStatus }, - { 項目: "下方圖表套用狀態_中文", 值: poStatusLabelZh(effectiveStatus) }, - { - 項目: "圓環篩選_供應商", - 值: - selectedSupplierId != null && selectedSupplierId > 0 - ? `supplierId=${selectedSupplierId}` - : selectedSupplierCode?.trim() - ? `supplierCode=${selectedSupplierCode.trim()}` - : "(未選)", - }, - { 項目: "圓環篩選_貨品", 值: selectedItemCode?.trim() ? selectedItemCode.trim() : "(未選)" }, - { - 項目: "下方查詢是否有效", - 值: drillQueryOpts === null ? "否(篩選交集無效,下方表為空)" : "是", - }, - { - 項目: "圖表說明", - 值: + meta("匯出時間_UTC", "Exported at (UTC)", exportedAtIso), + meta("訂單日期", "Order date", poTargetDate), + meta("多選_供應商", "Suppliers (multi-select)", filterSupplierText), + meta("多選_貨品", "Items (multi-select)", filterItemText), + meta("多選_採購單號", "Purchase order no. (multi-select)", filterPoText), + meta( + "預計送貨圓環_點選", + "Estimated delivery slice", + selectedEstimatedBucket ? bucketLabel(selectedEstimatedBucket, L) : none + ), + meta( + "實際已送貨圓環_點選狀態", + "Delivered slice status", + selectedStatus != null + ? poStatusLabel(selectedStatus, L) + : L( + `(未選;下方圖表預設狀態 ${defaultStatusLabel})`, + `(None selected; lower charts default to ${defaultStatusLabel})` + ) + ), + meta("下方圖表套用狀態_英文", "Applied status code", effectiveStatus), + meta("下方圖表套用狀態_中文", "Applied status label", poStatusLabel(effectiveStatus, L)), + meta( + "圓環篩選_供應商", + "Donut filter: supplier", + selectedSupplierId != null && selectedSupplierId > 0 + ? `supplierId=${selectedSupplierId}` + : selectedSupplierCode?.trim() + ? `supplierCode=${selectedSupplierCode.trim()}` + : none + ), + meta("圓環篩選_貨品", "Donut filter: item", selectedItemCode?.trim() ? selectedItemCode.trim() : none), + meta( + "下方查詢是否有效", + "Lower query valid", + drillQueryOpts === null + ? L("否(篩選交集無效,下方表為空)", "No (the filter intersection is empty, so the lower tables are blank)") + : L("是", "Yes") + ), + meta( + "圖表說明", + "Chart note", + L( "預計送貨圖:預計到貨日=訂單日期。實際已送貨圖:訂單日期。貨品/供應商/採購單表:依下方查詢與預計送貨扇形。採購單行明細:匯出當前列表中每張採購單之全部行。", - }, + "Estimated delivery chart: estimated arrival date equals the order date. Delivered chart: order date. Item, supplier, and purchase order tables follow the lower query and the estimated-delivery slice. Purchase order line details export every line of each purchase order in the current list." + ) + ), ]; const estimatedDonutRows = EST_BUCKETS.map((b, i) => ({ - 類別: bucketLabelZh(b), - bucket代碼: b, - 數量: estimatedChartSeries[i] ?? 0, + [L("類別", "Category")]: bucketLabel(b, L), + [L("bucket代碼", "Bucket Code")]: b, + [t("warehouse_qty")]: estimatedChartSeries[i] ?? 0, })); const actualStatusDonutRows = chartData.map((p) => ({ - 狀態中文: poStatusLabelZh(p.status), - status代碼: p.status, - 數量: p.count, + [L("狀態中文", "Status")]: poStatusLabel(p.status, L), + [L("status代碼", "Status Code")]: p.status, + [t("warehouse_qty")]: p.count, })); const itemSummaryRows = itemsSummary.map((i) => ({ - 貨品: i.itemCode, - 名稱: i.itemName, - 訂購數量: i.orderedQty, - 已收貨: i.receivedQty, - 待收貨: i.pendingQty, + [L("貨品", "Item")]: i.itemCode, + [L("名稱", "Name")]: i.itemName, + [L("訂購數量", "Ordered Qty")]: i.orderedQty, + [L("已收貨", "Received Qty")]: i.receivedQty, + [L("待收貨", "Pending Qty")]: i.pendingQty, UOM: i.uom, })); const supplierDistributionRows = supplierChartData.map((s) => ({ - 供應商: s.supplier, - 供應商編號: s.supplierCode, + [L("供應商", "Supplier")]: s.supplier, + [L("供應商編號", "Supplier Code")]: s.supplierCode, supplierId: s.supplierId ?? "", - 採購單數: s.count, - 總數量: s.totalQty, + [L("採購單數", "Purchase Order Count")]: s.count, + [L("總數量", "Total Qty")]: s.totalQty, })); const purchaseOrderListRows = poDetails.map((p) => ({ - 採購單號: p.purchaseOrderNo, - 狀態: poStatusLabelZh(p.status), - status代碼: p.status, - 訂單日期: p.orderDate, - 預計到貨日: p.estimatedArrivalDate, - 供應商編號: p.supplierCode, - 供應商名稱: p.supplierName, + [L("採購單號", "Purchase Order No.")]: p.purchaseOrderNo, + [L("狀態", "Status")]: poStatusLabel(p.status, L), + [L("status代碼", "Status Code")]: p.status, + [L("訂單日期", "Order Date")]: p.orderDate, + [L("預計到貨日", "Estimated Arrival Date")]: p.estimatedArrivalDate, + [L("供應商編號", "Supplier Code")]: p.supplierCode, + [L("供應商名稱", "Supplier Name")]: p.supplierName, supplierId: p.supplierId ?? "", - 項目數: p.itemCount, - 總數量: p.totalQty, + [L("項目數", "Item Count")]: p.itemCount, + [L("總數量", "Total Qty")]: p.totalQty, })); const purchaseOrderLineRows: Record[] = []; @@ -505,19 +529,19 @@ export default function PurchaseChartPage() { poDetails.map((po) => fetchPurchaseOrderItems(po.purchaseOrderId).then((lines) => lines.map((line) => ({ - 採購單號: po.purchaseOrderNo, - 採購單ID: po.purchaseOrderId, - 狀態: poStatusLabelZh(po.status), - 訂單日期: po.orderDate, - 預計到貨日: po.estimatedArrivalDate, - 供應商編號: po.supplierCode, - 供應商名稱: po.supplierName, - 貨品: line.itemCode, - 品名: line.itemName, + [L("採購單號", "Purchase Order No.")]: po.purchaseOrderNo, + [L("採購單ID", "Purchase Order ID")]: po.purchaseOrderId, + [L("狀態", "Status")]: poStatusLabel(po.status, L), + [L("訂單日期", "Order Date")]: po.orderDate, + [L("預計到貨日", "Estimated Arrival Date")]: po.estimatedArrivalDate, + [L("供應商編號", "Supplier Code")]: po.supplierCode, + [L("供應商名稱", "Supplier Name")]: po.supplierName, + [L("貨品", "Item")]: line.itemCode, + [L("品名", "Item Name")]: line.itemName, UOM: line.uom, - 訂購數量: line.orderedQty, - 已收貨: line.receivedQty, - 待收貨: line.pendingQty, + [L("訂購數量", "Ordered Qty")]: line.orderedQty, + [L("已收貨", "Received Qty")]: line.receivedQty, + [L("待收貨", "Pending Qty")]: line.pendingQty, })) ) ) @@ -536,14 +560,20 @@ export default function PurchaseChartPage() { purchaseOrderListRows, purchaseOrderLineRows, }, - `採購圖表總表_${poTargetDate}_${dayjs().format("HHmmss")}` + L( + `採購圖表總表_${poTargetDate}_${dayjs().format("HHmmss")}`, + `Purchase_Chart_${poTargetDate}_${dayjs().format("HHmmss")}` + ), + L ); } catch (err) { - setError(err instanceof Error ? err.message : "總表匯出失敗"); + setError(err instanceof Error ? err.message : L("總表匯出失敗", "Master export failed")); } finally { setMasterExportLoading(false); } }, [ + L, + t, poTargetDate, filterOptions.suppliers, filterOptions.items, @@ -571,23 +601,26 @@ export default function PurchaseChartPage() { /** 下方三張圖:僅選預計送貨扇形時,資料依 bucket 不再套用「實際已送貨」單一狀態。 */ const lowerChartsTitlePrefix = React.useMemo(() => { if (selectedEstimatedBucket) { - return `預計送貨「${bucketLabelZh(selectedEstimatedBucket)}」`; + const name = bucketLabel(selectedEstimatedBucket, L); + return L(`預計送貨「${name}」`, `Estimated delivery “${name}”`); } - return `實際已送貨 ${poStatusLabelZh(effectiveStatus)}`; - }, [selectedEstimatedBucket, effectiveStatus]); + const name = poStatusLabel(effectiveStatus, L); + return L(`實際已送貨 ${name}`, `Delivered ${name}`); + }, [selectedEstimatedBucket, effectiveStatus, L]); const lowerChartsDefaultStatusHint = React.useMemo(() => { if (selectedEstimatedBucket) return ""; if (selectedStatus) return ""; - return `(預設狀態:${poStatusLabelZh(DEFAULT_DRILL_STATUS)})`; - }, [selectedEstimatedBucket, selectedStatus]); + const name = poStatusLabel(DEFAULT_DRILL_STATUS, L); + return L(`(預設狀態:${name})`, ` (Default status: ${name})`); + }, [selectedEstimatedBucket, selectedStatus, L]); const filterHint = [ - selectedItemCode ? `貨品: ${selectedItemCode}` : null, + selectedItemCode ? L(`貨品: ${selectedItemCode}`, `Item: ${selectedItemCode}`) : null, selectedSupplierId != null && selectedSupplierId > 0 - ? `供應商 id: ${selectedSupplierId}` + ? L(`供應商 id: ${selectedSupplierId}`, `Supplier id: ${selectedSupplierId}`) : selectedSupplierCode - ? `供應商 code: ${selectedSupplierCode}` + ? L(`供應商 code: ${selectedSupplierCode}`, `Supplier code: ${selectedSupplierCode}`) : null, ] .filter(Boolean) @@ -605,7 +638,7 @@ export default function PurchaseChartPage() { return ( - {PAGE_TITLE} + {t("pageTitle_purchase")} {error && ( setError(null)}> @@ -615,7 +648,10 @@ export default function PurchaseChartPage() { - 上方「預計送貨」與「實際已送貨」依查詢日期與篩選條件;點擊圓環可篩選下方圖表(與其他條件交集)。 + {L( + "上方「預計送貨」與「實際已送貨」依查詢日期與篩選條件;點擊圓環可篩選下方圖表(與其他條件交集)。", + "Estimated delivery and delivered charts above follow the query date and filters. Click a slice to filter the charts below (combined with the other filters)." + )} {(hasBarFilters || hasChartDrill) && ( )} @@ -637,7 +673,7 @@ export default function PurchaseChartPage() { setPoTargetDate(e.target.value)} @@ -658,7 +694,7 @@ export default function PurchaseChartPage() { )) } - renderInput={(params) => } + renderInput={(params) => } /> )) } - renderInput={(params) => } + renderInput={(params) => } /> )) } - renderInput={(params) => } + renderInput={(params) => } /> ({ - 類別: bucketLabelZh(b), - 數量: estimatedChartSeries[i] ?? 0, + [L("類別", "Category")]: bucketLabel(b, L), + [t("warehouse_qty")]: estimatedChartSeries[i] ?? 0, }))} > {estimatedLoading ? ( @@ -720,7 +756,7 @@ export default function PurchaseChartPage() { }, animations: { enabled: false }, }, - labels: EST_BUCKETS.map(bucketLabelZh), + labels: EST_BUCKETS.map((b) => bucketLabel(b, L)), colors: ESTIMATE_DONUT_COLORS, legend: { position: "bottom" }, plotOptions: { @@ -730,7 +766,7 @@ export default function PurchaseChartPage() { show: true, total: { show: true, - label: "預計送貨", + label: L("預計送貨", "Estimated delivery"), }, }, }, @@ -747,9 +783,12 @@ export default function PurchaseChartPage() { ({ 狀態: poStatusLabelZh(p.status), 數量: p.count }))} + title={L("實際已送貨(依預計到貨日或實收日)", "Delivered (by Estimated Arrival or Receipt Date)")} + exportFilename={L("採購_實際已送貨", "Purchase_Delivered")} + exportData={chartData.map((p) => ({ + [L("狀態", "Status")]: poStatusLabel(p.status, L), + [t("warehouse_qty")]: p.count, + }))} > {loading ? ( @@ -770,7 +809,7 @@ export default function PurchaseChartPage() { }, animations: { enabled: false }, }, - labels: chartData.map((p) => poStatusLabelZh(p.status)), + labels: chartData.map((p) => poStatusLabel(p.status, L)), colors: chartData.map((_, i) => STATUS_DONUT_COLORS[i % STATUS_DONUT_COLORS.length]), legend: { position: "bottom" }, }} @@ -787,10 +826,16 @@ export default function PurchaseChartPage() { {selectedEstimatedBucket && ( - 「{bucketLabelZh(selectedEstimatedBucket)}」關聯對象 + {L( + `「${bucketLabel(selectedEstimatedBucket, L)}」關聯對象`, + `Related records for “${bucketLabel(selectedEstimatedBucket, L)}”` + )} - 條件與左側「預計送貨」圓環一致:預計到貨日 = 查詢日期({poTargetDate}),並含上方供應商/貨品/採購單號多選。 + {L( + `條件與左側「預計送貨」圓環一致:預計到貨日 = 查詢日期(${poTargetDate}),並含上方供應商/貨品/採購單號多選。`, + `Same filters as the Estimated Delivery chart: estimated arrival date = query date (${poTargetDate}), plus the supplier, item, and purchase order multi-select above.` + )} {eaBreakdownLoading ? ( @@ -800,15 +845,15 @@ export default function PurchaseChartPage() { - 供應商 + {L("供應商", "Supplier")}
- 編號 - 名稱 - 採購單數 + {L("編號", "Code")} + {L("名稱", "Name")} + {L("採購單數", "Purchase Order Count")} @@ -816,7 +861,7 @@ export default function PurchaseChartPage() { - 無 + {L("無", "None")} @@ -835,16 +880,16 @@ export default function PurchaseChartPage() { - 貨品 + {L("貨品", "Item")}
- 貨品編號 - 名稱 - 採購單數 - 總數量 + {L("貨品編號", "Item Code")} + {L("名稱", "Name")} + {L("採購單數", "Purchase Order Count")} + {L("總數量", "Total Qty")} @@ -852,7 +897,7 @@ export default function PurchaseChartPage() { - 無 + {L("無", "None")} @@ -872,16 +917,16 @@ export default function PurchaseChartPage() { - 採購單 + {L("採購單", "Purchase Order")}
- 採購單號 - 供應商 - 狀態 - 訂單日期 + {L("採購單號", "Purchase Order No.")} + {L("供應商", "Supplier")} + {L("狀態", "Status")} + {L("訂單日期", "Order Date")} @@ -889,7 +934,7 @@ export default function PurchaseChartPage() { - 無 + {L("無", "None")} @@ -898,7 +943,7 @@ export default function PurchaseChartPage() { {po.purchaseOrderNo} {`${po.supplierCode} ${po.supplierName}`.trim()} - {poStatusLabelZh(po.status)} + {poStatusLabel(po.status, L)} {po.orderDate} )) @@ -916,40 +961,52 @@ export default function PurchaseChartPage() { {selectedEstimatedBucket && ( - 下方圖表已依「預計送貨」篩選:{bucketLabelZh(selectedEstimatedBucket)} - (預計到貨日 = 查詢日期,並含多選;此時不再套用右側「實際已送貨」狀態;再點同一扇形可取消)。 + {L( + `下方圖表已依「預計送貨」篩選:${bucketLabel(selectedEstimatedBucket, L)}(預計到貨日 = 查詢日期,並含多選;此時不再套用右側「實際已送貨」狀態;再點同一扇形可取消)。`, + `Lower charts are filtered by estimated delivery: ${bucketLabel(selectedEstimatedBucket, L)} (estimated arrival date = query date, plus multi-select). The delivered status on the right is not applied. Click the same slice again to clear.` + )} )} {selectedStatus != null && ( - 下方圖表已依「實際已送貨」所選狀態:{poStatusLabelZh(selectedStatus)}(再點同一狀態可取消)。 + {L( + `下方圖表已依「實際已送貨」所選狀態:${poStatusLabel(selectedStatus, L)}(再點同一狀態可取消)。`, + `Lower charts are filtered by delivered status: ${poStatusLabel(selectedStatus, L)}. Click the same status again to clear.` + )} )} )} ({ - 貨品: i.itemCode, - 名稱: i.itemName, - 訂購數量: i.orderedQty, - 已收貨: i.receivedQty, + [L("貨品", "Item")]: i.itemCode, + [L("名稱", "Name")]: i.itemName, + [L("訂購數量", "Ordered Qty")]: i.orderedQty, + [L("已收貨", "Received Qty")]: i.receivedQty, UOM: i.uom, }))} > {drillQueryOpts === null ? ( - 無符合交集的篩選(請調整上方條件或圖表點選) + + {L("無符合交集的篩選(請調整上方條件或圖表點選)", "No rows match this filter combination. Adjust the filters above or the chart selection.")} + ) : itemsSummaryLoading ? ( ) : itemsSummary.length === 0 ? ( - 無資料(請確認訂單日期{selectedEstimatedBucket ? "與篩選" : "與狀態"}) + {selectedEstimatedBucket + ? L("無資料(請確認訂單日期與篩選)", "No data (check the order date and filters).") + : L("無資料(請確認訂單日期與狀態)", "No data (check the order date and status).")} ) : ( ({ - 供應商: s.supplier, - 採購單數: s.count, - 總數量: s.totalQty, + [L("供應商", "Supplier")]: s.supplier, + [L("採購單數", "Purchase Order Count")]: s.count, + [L("總數量", "Total Qty")]: s.totalQty, }))} > {drillQueryOpts === null ? ( - 無符合交集的篩選 + {L("無符合交集的篩選", "No rows match this filter combination.")} ) : poDetailsLoading ? ( ) : supplierChartData.length === 0 ? ( - 無供應商資料(請先確認上方貨品篩選或日期) + + {L("無供應商資料(請先確認上方貨品篩選或日期)", "No supplier data. Check the item filter above or the date.")} + ) : ( ({ - 採購單號: p.purchaseOrderNo, - 供應商: `${p.supplierCode} ${p.supplierName}`.trim(), - 總數量: p.totalQty, - 項目數: p.itemCount, - 訂單日期: p.orderDate, + [L("採購單號", "Purchase Order No.")]: p.purchaseOrderNo, + [L("供應商", "Supplier")]: `${p.supplierCode} ${p.supplierName}`.trim(), + [L("總數量", "Total Qty")]: p.totalQty, + [L("項目數", "Item Count")]: p.itemCount, + [L("訂單日期", "Order Date")]: p.orderDate, }))} > {drillQueryOpts === null ? ( - 無符合交集的篩選 + {L("無符合交集的篩選", "No rows match this filter combination.")} ) : poDetailsLoading ? ( ) : poDetails.length === 0 ? ( - 無採購單。請確認該「訂單日期」是否有此狀態的採購單。 + {L( + "無採購單。請確認該「訂單日期」是否有此狀態的採購單。", + "No purchase orders. Check whether this order date has purchase orders in this status." + )} ) : ( p.totalQty), }, ]} @@ -1090,14 +1160,20 @@ export default function PurchaseChartPage() { {selectedPo && ( ({ - 貨品: i.itemCode, - 名稱: i.itemName, - 訂購數量: i.orderedQty, - 已收貨: i.receivedQty, - 待收貨: i.pendingQty, + [L("貨品", "Item")]: i.itemCode, + [L("名稱", "Name")]: i.itemName, + [L("訂購數量", "Ordered Qty")]: i.orderedQty, + [L("已收貨", "Received Qty")]: i.receivedQty, + [L("待收貨", "Pending Qty")]: i.pendingQty, UOM: i.uom, }))} > @@ -1114,8 +1190,8 @@ export default function PurchaseChartPage() { dataLabels: { enabled: false }, }} series={[ - { name: "訂購數量", data: poLineItems.map((i) => i.orderedQty) }, - { name: "待收貨", data: poLineItems.map((i) => i.pendingQty) }, + { name: L("訂購數量", "Ordered Qty"), data: poLineItems.map((i) => i.orderedQty) }, + { name: L("待收貨", "Pending Qty"), data: poLineItems.map((i) => i.pendingQty) }, ]} type="bar" width="100%" diff --git a/src/app/(main)/chart/warehouse/page.tsx b/src/app/(main)/chart/warehouse/page.tsx index 06093844..3456128c 100644 --- a/src/app/(main)/chart/warehouse/page.tsx +++ b/src/app/(main)/chart/warehouse/page.tsx @@ -10,13 +10,13 @@ import { fetchStockBalanceTrend, fetchConsumptionTrendByMonth, } from "@/app/api/chart/client"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import ChartCard from "../_components/ChartCard"; import DateRangeSelect from "../_components/DateRangeSelect"; import { toDateRange, DEFAULT_RANGE_DAYS, ITEM_CODE_DEBOUNCE_MS } from "../_components/constants"; import SafeApexCharts from "@/components/charts/SafeApexCharts"; -const PAGE_TITLE = "庫存與倉儲"; - type Criteria = { stockTxn: { rangeDays: number }; stockInOut: { rangeDays: number }; @@ -32,6 +32,8 @@ const defaultCriteria: Criteria = { }; export default function WarehouseChartPage() { + const { t } = useTranslation("chart"); + const L = useUiText(); const [criteria, setCriteria] = useState(defaultCriteria); const [itemCodeBalance, setItemCodeBalance] = useState(""); const [debouncedItemCodeBalance, setDebouncedItemCodeBalance] = useState(""); @@ -160,7 +162,7 @@ export default function WarehouseChartPage() { return ( - {PAGE_TITLE} + {t("pageTitle_warehouse")} {error && ( setError(null)}> @@ -169,9 +171,14 @@ export default function WarehouseChartPage() { )} ({ 日期: s.date, 入庫: s.inQty, 出庫: s.outQty, 合計: s.totalQty }))} + title={t("warehouse_stockTxnByDate")} + exportFilename={t("warehouse_stockTxnByDate_export")} + exportData={chartData.stockTxn.map((s) => ({ + [L("日期", "Date")]: s.date, + [t("series_inbound")]: s.inQty, + [t("series_outbound")]: s.outQty, + [t("series_total")]: s.totalQty, + }))} filters={ s.date) }, - yaxis: { title: { text: "數量" } }, + yaxis: { title: { text: t("warehouse_qty") } }, stroke: { curve: "smooth" }, dataLabels: { enabled: false }, }} series={[ - { name: "入庫", data: chartData.stockTxn.map((s) => s.inQty) }, - { name: "出庫", data: chartData.stockTxn.map((s) => s.outQty) }, - { name: "合計", data: chartData.stockTxn.map((s) => s.totalQty) }, + { name: t("series_inbound"), data: chartData.stockTxn.map((s) => s.inQty) }, + { name: t("series_outbound"), data: chartData.stockTxn.map((s) => s.outQty) }, + { name: t("series_total"), data: chartData.stockTxn.map((s) => s.totalQty) }, ]} type="line" width="100%" @@ -203,9 +210,13 @@ export default function WarehouseChartPage() { ({ 日期: s.date, 入庫: s.inQty, 出庫: s.outQty }))} + title={t("warehouse_stockInOutByDate")} + exportFilename={L("入庫與出庫_按日期", "Stock_In_and_Out_By_Date")} + exportData={chartData.stockInOut.map((s) => ({ + [L("日期", "Date")]: s.date, + [t("series_inbound")]: s.inQty, + [t("series_outbound")]: s.outQty, + }))} filters={ s.date) }, - yaxis: { title: { text: "數量" } }, + yaxis: { title: { text: t("warehouse_qty") } }, stroke: { curve: "smooth" }, dataLabels: { enabled: false }, }} series={[ - { name: "入庫", data: chartData.stockInOut.map((s) => s.inQty) }, - { name: "出庫", data: chartData.stockInOut.map((s) => s.outQty) }, + { name: t("series_inbound"), data: chartData.stockInOut.map((s) => s.inQty) }, + { name: t("series_outbound"), data: chartData.stockInOut.map((s) => s.outQty) }, ]} type="area" width="100%" @@ -236,9 +247,12 @@ export default function WarehouseChartPage() { ({ 日期: b.date, 餘額: b.balance }))} + title={t("warehouse_balanceTrend")} + exportFilename={t("warehouse_balanceTrend_export")} + exportData={chartData.balance.map((b) => ({ + [L("日期", "Date")]: b.date, + [t("series_balance")]: b.balance, + }))} filters={ <> setItemCodeBalance(e.target.value)} sx={{ minWidth: 180 }} @@ -263,11 +277,11 @@ export default function WarehouseChartPage() { options={{ chart: { type: "line" }, xaxis: { categories: chartData.balance.map((b) => b.date) }, - yaxis: { title: { text: "餘額" } }, + yaxis: { title: { text: t("series_balance") } }, stroke: { curve: "smooth" }, dataLabels: { enabled: false }, }} - series={[{ name: "餘額", data: chartData.balance.map((b) => b.balance) }]} + series={[{ name: t("series_balance"), data: chartData.balance.map((b) => b.balance) }]} type="line" width="100%" height={320} @@ -276,18 +290,21 @@ export default function WarehouseChartPage() { s.data.map((qty, i) => ({ - 月份: chartData.consumptionByItems!.months[i], - 物料編碼: s.name, - 出庫量: qty, + [t("series_month")]: chartData.consumptionByItems!.months[i], + [L("物料編碼", "Item Code")]: s.name, + [L("出庫量", "Outbound Qty")]: qty, })) ) - : chartData.consumption.map((c) => ({ 月份: c.month, 出庫量: c.outQty })) + : chartData.consumption.map((c) => ({ + [t("series_month")]: c.month, + [L("出庫量", "Outbound Qty")]: c.outQty, + })) } filters={ <> @@ -298,15 +315,19 @@ export default function WarehouseChartPage() { setConsumptionItemCodeInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addConsumptionItem())} sx={{ minWidth: 180 }} /> {consumptionItemCodes.map((code) => ( c.month) }, - yaxis: { title: { text: "出庫量" } }, + yaxis: { title: { text: L("出庫量", "Outbound Qty") } }, plotOptions: { bar: { columnWidth: "60%" } }, dataLabels: { enabled: false }, }} - series={[{ name: "出庫量", data: chartData.consumption.map((c) => c.outQty) }]} + series={[{ name: L("出庫量", "Outbound Qty"), data: chartData.consumption.map((c) => c.outQty) }]} type="bar" width="100%" height={320} diff --git a/src/app/(main)/laserPrint/page.tsx b/src/app/(main)/laserPrint/page.tsx index 38ad70fb..443712ee 100644 --- a/src/app/(main)/laserPrint/page.tsx +++ b/src/app/(main)/laserPrint/page.tsx @@ -1,20 +1,18 @@ -import { I18nProvider } from "@/i18n"; +import { detectLanguage, I18nProvider } from "@/i18n"; import { Metadata } from "next"; -import LaserPrintSearch from "@/components/LaserPrint/LaserPrintSearch"; -import { Stack, Typography } from "@mui/material"; +import LaserPrintSearch, { + LaserPrintPageTitle, +} from "@/components/LaserPrint/LaserPrintSearch"; -export const metadata: Metadata = { - title: "激光機", -}; +export async function generateMetadata(): Promise { + const english = (await detectLanguage()).toLowerCase().startsWith("en"); + return { title: english ? "Laser printer" : "激光機" }; +} export default async function LaserPrintPage() { return ( - - - 激光機 - - + ); diff --git a/src/app/(main)/m18Syn/page.tsx b/src/app/(main)/m18Syn/page.tsx index c0b68266..fc172ff5 100644 --- a/src/app/(main)/m18Syn/page.tsx +++ b/src/app/(main)/m18Syn/page.tsx @@ -4,6 +4,7 @@ import React, { useRef, useState } from "react"; import { Box, Button, Paper, Stack, Tab, Tabs, TextField, Typography } from "@mui/material"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; +import { useUiText } from "@/i18n/useUiText"; interface TabPanelProps { children?: React.ReactNode; @@ -28,6 +29,7 @@ function TabPanel(props: TabPanelProps) { /** FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13 */ export default function M18SynPage() { + const L = useUiText(); const [tabValue, setTabValue] = useState(0); type SyncResultDto = { @@ -45,19 +47,30 @@ export default function M18SynPage() { | { kind: "mixed"; message: string; totalSuccess: number; totalFail: number } | { kind: "raw"; message: string }; + const docEn = (docName: string) => { + if (docName === "採購單") return "purchase order"; + if (docName === "送貨訂單") return "delivery order"; + if (docName === "產品") return "product"; + return docName; + }; + const parseSyncResult = (docName: string, rawText: string): ParsedSyncResult => { const text = rawText?.trim() ?? ""; - if (!text) return { kind: "raw", message: "未收到回應" }; + if (!text) return { kind: "raw", message: L("未收到回應", "No response received") }; try { const obj = JSON.parse(text) as Partial; const totalSuccess = Number(obj.totalSuccess ?? 0); const totalFail = Number(obj.totalFail ?? 0); const totalProcessed = Number(obj.totalProcessed ?? 0); const query = (obj.query ?? "").toString(); + const enName = docEn(docName); // newOnly skip message from backend if (query.includes("skipped") && query.includes("already exists")) { - return { kind: "exists", message: `${docName}已存在系統` }; + return { + kind: "exists", + message: L(`${docName}已存在系統`, `This ${enName} already exists in the system`), + }; } // "not found in M18" (sync-by-code returns processed=1, fail=1) @@ -68,16 +81,36 @@ export default function M18SynPage() { totalFail === 1 && query.includes("code=equal=") ) { - return { kind: "not_found", message: `在M18找不到${docName}` }; + return { + kind: "not_found", + message: L(`在M18找不到${docName}`, "Delivery order not found in M18"), + }; } if (totalSuccess > 0 && totalFail === 0) { - return { kind: "success", totalSuccess, message: `成功同步: ${totalSuccess}張${docName}` }; + const enCount = totalSuccess === 1 ? `1 ${enName}` : `${totalSuccess} ${enName}s`; + return { + kind: "success", + totalSuccess, + message: L(`成功同步: ${totalSuccess}張${docName}`, `Synced successfully: ${enCount}`), + }; } if (totalSuccess === 0 && totalFail > 0) { - return { kind: "fail", totalFail, message: `失敗: 無法同步${docName}` }; + return { + kind: "fail", + totalFail, + message: L(`失敗: 無法同步${docName}`, `Failed: could not sync the ${enName}`), + }; } - return { kind: "mixed", totalSuccess, totalFail, message: `完成: 成功 ${totalSuccess} / 失敗 ${totalFail} (${docName})` }; + return { + kind: "mixed", + totalSuccess, + totalFail, + message: L( + `完成: 成功 ${totalSuccess} / 失敗 ${totalFail} (${docName})`, + `Finished: ${totalSuccess} succeeded / ${totalFail} failed (${enName})`, + ), + }; } catch { // Non-JSON error body or plain text return { kind: "raw", message: text }; @@ -172,16 +205,16 @@ export default function M18SynPage() { else otherFailCodes.push(code); const perCodeMsg = - parsed.kind === "success" ? "成功同步" : + parsed.kind === "success" ? L("成功同步", "Synced successfully") : parsed.kind === "not_found" ? parsed.message : parsed.message; outputs.push(`${code}: ${perCodeMsg}${response.ok ? "" : ` (HTTP ${response.status})`}`); } outputs.push(""); - outputs.push(`共${okCodes.length}張送貨訂單成功`); - if (notFoundCodes.length > 0) outputs.push(`共${notFoundCodes.length}張在M18找不到訂單 (${notFoundCodes.join(", ")})`); - if (otherFailCodes.length > 0) outputs.push(`共${otherFailCodes.length}張同步失敗 (${otherFailCodes.join(", ")})`); + outputs.push(L(`共${okCodes.length}張送貨訂單成功`, `${okCodes.length} delivery order${okCodes.length === 1 ? "" : "s"} synced successfully`)); + if (notFoundCodes.length > 0) outputs.push(L(`共${notFoundCodes.length}張在M18找不到訂單 (${notFoundCodes.join(", ")})`, `${notFoundCodes.length} order${notFoundCodes.length === 1 ? "" : "s"} not found in M18 (${notFoundCodes.join(", ")})`)); + if (otherFailCodes.length > 0) outputs.push(L(`共${otherFailCodes.length}張同步失敗 (${otherFailCodes.join(", ")})`, `${otherFailCodes.length} failed to sync (${otherFailCodes.join(", ")})`)); setM18DoSyncResult(outputs.join("\n")); } catch (e) { @@ -198,7 +231,7 @@ export default function M18SynPage() { if (m18DoExtraInFlightRef.current) return; const raw = m18DoExtraCode.trim(); if (!raw) { - alert("Please enter DO / shop PO code(s) (加單)."); + alert(L("Please enter DO / shop PO code(s) (加單).", "Please enter a delivery order or shop PO code for an extra order.")); return; } const codes = raw @@ -232,8 +265,8 @@ export default function M18SynPage() { else otherFailCodes.push(code); const perCodeMsg = - parsed.kind === "success" ? "成功同步" : - parsed.kind === "exists" ? `失敗: ${parsed.message}` : + parsed.kind === "success" ? L("成功同步", "Synced successfully") : + parsed.kind === "exists" ? L(`失敗: ${parsed.message}`, `Failed: ${parsed.message}`) : parsed.kind === "not_found" ? parsed.message : parsed.message; outputs.push(`${code}: ${perCodeMsg}${response.ok ? "" : ` (HTTP ${response.status})`}`); @@ -241,15 +274,15 @@ export default function M18SynPage() { // Summary outputs.push(""); - outputs.push(`共${okCodes.length}張送貨訂單成功`); - if (notFoundCodes.length > 0) outputs.push(`共${notFoundCodes.length}張在M18找不到訂單 (${notFoundCodes.join(", ")})`); - if (existsCodes.length > 0) outputs.push(`共${existsCodes.length}張訂單已存在系統 (${existsCodes.join(", ")})`); - if (otherFailCodes.length > 0) outputs.push(`共${otherFailCodes.length}張同步失敗 (${otherFailCodes.join(", ")})`); + outputs.push(L(`共${okCodes.length}張送貨訂單成功`, `${okCodes.length} delivery order${okCodes.length === 1 ? "" : "s"} synced successfully`)); + if (notFoundCodes.length > 0) outputs.push(L(`共${notFoundCodes.length}張在M18找不到訂單 (${notFoundCodes.join(", ")})`, `${notFoundCodes.length} order${notFoundCodes.length === 1 ? "" : "s"} not found in M18 (${notFoundCodes.join(", ")})`)); + if (existsCodes.length > 0) outputs.push(L(`共${existsCodes.length}張訂單已存在系統 (${existsCodes.join(", ")})`, `${existsCodes.length} order${existsCodes.length === 1 ? "" : "s"} already exist in the system (${existsCodes.join(", ")})`)); + if (otherFailCodes.length > 0) outputs.push(L(`共${otherFailCodes.length}張同步失敗 (${otherFailCodes.join(", ")})`, `${otherFailCodes.length} failed to sync (${otherFailCodes.join(", ")})`)); setM18DoExtraSyncResult(outputs.join("\n")); } catch (e) { console.error("M18 DO (加單) Sync By Code Error:", e); - alert("M18 DO (加單) sync failed. Check console/network."); + alert(L("M18 DO (加單) sync failed. Check console/network.", "Extra delivery order sync from M18 failed. Check the console or network.")); } finally { setIsSyncingM18DoExtra(false); m18DoExtraInFlightRef.current = false; @@ -300,18 +333,21 @@ export default function M18SynPage() { M18 Sync (by code) - Requires 行政 (ADMIN) or M18同步 (M18_SYNC). Sync Purchase Order, Delivery Order, or product/material from M18 using document or item code. + {L( + "Requires 行政 (ADMIN) or M18同步 (M18_SYNC). Sync Purchase Order, Delivery Order, or product/material from M18 using document or item code.", + "Requires Admin (ADMIN) or M18 sync (M18_SYNC). Sync a purchase order, delivery order, or product/material from M18 using a document or item code.", + )} setTabValue(v)} aria-label="M18 sync by code" centered variant="fullWidth"> - - - + + + -
+
-
+
setM18DoCode(e.target.value)} - placeholder="可輸入多個 code,用逗號或換行分隔" + placeholder={L("可輸入多個 code,用逗號或換行分隔", "Enter more than one code, separated by commas or new lines")} fullWidth multiline minRows={6} @@ -373,13 +409,13 @@ export default function M18SynPage() { -
+
setM18DoExtraCode(e.target.value)} - placeholder="可輸入多個 code,用逗號或換行分隔" + placeholder={L("可輸入多個 code,用逗號或換行分隔", "Enter more than one code, separated by commas or new lines")} fullWidth multiline minRows={6} @@ -392,7 +428,7 @@ export default function M18SynPage() { onClick={handleSyncM18DoExtraByCode} disabled={isSyncingM18DoExtra} > - {isSyncingM18DoExtra ? "Syncing..." : "Sync 送貨訂單(加單)from M18"} + {isSyncingM18DoExtra ? "Syncing..." : L("Sync 送貨訂單(加單)from M18", "Sync extra delivery orders from M18")} diff --git a/src/app/api/bagPrint/actions.ts b/src/app/api/bagPrint/actions.ts index a0a6053f..b5f02240 100644 --- a/src/app/api/bagPrint/actions.ts +++ b/src/app/api/bagPrint/actions.ts @@ -2,6 +2,30 @@ import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; +import { I18N_COOKIE_NAME, normalizeAppLanguage } from "@/i18n/locale"; + +/** + * Optional `language` overrides detection. When omitted, use the page language + * (``), then the i18next cookie. Missing both stays Chinese. + */ +function resolveUiLanguage(language?: string | null): string { + if (typeof language === "string" && language.trim()) return language; + if (typeof document !== "undefined") { + const fromHtml = document.documentElement?.lang?.trim(); + if (fromHtml) return fromHtml; + const raw = document.cookie + .split("; ") + .find((part) => part.startsWith(`${I18N_COOKIE_NAME}=`)) + ?.slice(I18N_COOKIE_NAME.length + 1); + const fromCookie = normalizeAppLanguage(raw ? decodeURIComponent(raw) : null); + if (fromCookie) return fromCookie; + } + return "zh"; +} + +function uiText(zh: string, en: string, language?: string | null): string { + return resolveUiLanguage(language).toLowerCase().startsWith("en") ? en : zh; +} export interface JobOrderListItem { id: number; @@ -87,12 +111,17 @@ export async function pushOnPackTextQrZipToNgpcl(request: OnPackQrDownloadReques } /** Readable message when ZIP download returns non-OK (plain text, JSON error body, or generic). */ -async function zipDownloadError(res: Response): Promise { - return parseBagPrintApiError(res, "下載"); +async function zipDownloadError(res: Response, language?: string): Promise { + return parseBagPrintApiError(res, "下載", "Download", language); } /** Backend ErrorRes is `{ timestamp, traceId }` with no message; avoid calling that a ZIP download failure. */ -async function parseBagPrintApiError(res: Response, action: string): Promise { +async function parseBagPrintApiError( + res: Response, + actionZh: string, + actionEn: string, + language?: string, +): Promise { const text = await res.text(); const ct = res.headers.get("content-type") ?? ""; if (ct.includes("application/json")) { @@ -106,7 +135,11 @@ async function parseBagPrintApiError(res: Response, action: string): Promise 0) { return new Error( - `${action}失敗(HTTP ${res.status})。請重啟後端以執行 Liquibase,或查看日誌 traceId ${j.traceId}。`, + uiText( + `${actionZh}失敗(HTTP ${res.status})。請重啟後端以執行 Liquibase,或查看日誌 traceId ${j.traceId}。`, + `${actionEn} failed (HTTP ${res.status}). Restart the backend so Liquibase can run, or check the log for traceId ${j.traceId}.`, + language, + ), ); } } catch { @@ -116,7 +149,13 @@ async function parseBagPrintApiError(res: Response, action: string): Promise 0 && text.length < 800 && !text.trim().startsWith("{")) { return new Error(text); } - return new Error(`${action}失敗(HTTP ${res.status})。請查看後端日誌或確認資料庫已執行 Liquibase 更新。`); + return new Error( + uiText( + `${actionZh}失敗(HTTP ${res.status})。請查看後端日誌或確認資料庫已執行 Liquibase 更新。`, + `${actionEn} failed (HTTP ${res.status}). Check the backend log, or confirm the database has the Liquibase update.`, + language, + ), + ); } /** @@ -152,6 +191,7 @@ export async function checkPrinterStatus( export async function downloadOnPackQrZip( request: OnPackQrDownloadRequest, + language?: string, ): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr`; const res = await clientAuthFetch(url, { @@ -161,7 +201,7 @@ export async function downloadOnPackQrZip( }); if (!res.ok) { - throw await zipDownloadError(res); + throw await zipDownloadError(res, language); } return res.blob(); @@ -183,6 +223,7 @@ function skippedWithoutExpiryFromResponse(res: Response): string[] { /** OnPack QR — same as QR ZIP, plus LOGO_EXP BMP from item_default_shelf_life. */ export async function downloadOnPackQrZipWithExpiry( request: OnPackQrDownloadRequest, + language?: string, ): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-with-expiry`; const res = await clientAuthFetch(url, { @@ -192,7 +233,7 @@ export async function downloadOnPackQrZipWithExpiry( }); if (!res.ok) { - throw await zipDownloadError(res); + throw await zipDownloadError(res, language); } return { @@ -204,6 +245,7 @@ export async function downloadOnPackQrZipWithExpiry( /** OnPack text QR template (`onpack2030_2`), no separate .bmp */ export async function downloadOnPackTextQrZip( request: OnPackQrDownloadRequest, + language?: string, ): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-text`; const res = await clientAuthFetch(url, { @@ -213,7 +255,7 @@ export async function downloadOnPackTextQrZip( }); if (!res.ok) { - throw await zipDownloadError(res); + throw await zipDownloadError(res, language); } return res.blob(); @@ -222,6 +264,7 @@ export async function downloadOnPackTextQrZip( /** OnPack text QR — same as text ZIP, plus TEXT_EXP from item_default_shelf_life. */ export async function downloadOnPackTextQrZipWithExpiry( request: OnPackQrDownloadRequest, + language?: string, ): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-text-with-expiry`; const res = await clientAuthFetch(url, { @@ -231,7 +274,7 @@ export async function downloadOnPackTextQrZipWithExpiry( }); if (!res.ok) { - throw await zipDownloadError(res); + throw await zipDownloadError(res, language); } return { @@ -288,11 +331,11 @@ export type OnPackExpiryItemCodeUpdate = { useMinus18?: boolean; }; -export async function fetchOnPackExpiryCodes(machine: OnPackMachine = "juice"): Promise { +export async function fetchOnPackExpiryCodes(machine: OnPackMachine = "juice", language?: string): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/expiry-codes?machine=${encodeURIComponent(machine)}`; const res = await clientAuthFetch(url, { method: "GET" }); if (!res.ok) { - throw await parseBagPrintApiError(res, "讀取到期日 ZIP 品號"); + throw await parseBagPrintApiError(res, "讀取到期日 ZIP 品號", "Load expiry-date ZIP item codes", language); } return (await res.json()) as OnPackExpiryItemCodeDto[]; } @@ -300,6 +343,7 @@ export async function fetchOnPackExpiryCodes(machine: OnPackMachine = "juice"): export async function addOnPackExpiryCode( itemCode: string, machine: OnPackMachine = "juice", + language?: string, ): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/expiry-codes`; const res = await clientAuthFetch(url, { @@ -308,13 +352,14 @@ export async function addOnPackExpiryCode( body: JSON.stringify({ itemCode, machine }), }); if (!res.ok) { - throw await parseBagPrintApiError(res, "新增到期日 ZIP 品號"); + throw await parseBagPrintApiError(res, "新增到期日 ZIP 品號", "Add expiry-date ZIP item code", language); } return (await res.json()) as OnPackExpiryItemCodeDto; } export async function updateOnPackExpiryCode( body: OnPackExpiryItemCodeUpdate, + language?: string, ): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/expiry-codes`; const res = await clientAuthFetch(url, { @@ -323,7 +368,7 @@ export async function updateOnPackExpiryCode( body: JSON.stringify(body), }); if (!res.ok) { - throw await parseBagPrintApiError(res, "更新到期日 ZIP 品號"); + throw await parseBagPrintApiError(res, "更新到期日 ZIP 品號", "Update expiry-date ZIP item code", language); } return (await res.json()) as OnPackExpiryItemCodeDto; } @@ -331,29 +376,30 @@ export async function updateOnPackExpiryCode( export async function deleteOnPackExpiryCode( itemCode: string, machine: OnPackMachine = "juice", + language?: string, ): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/expiry-codes?machine=${encodeURIComponent(machine)}&itemCode=${encodeURIComponent(itemCode)}`; const res = await clientAuthFetch(url, { method: "DELETE" }); if (!res.ok && res.status !== 204) { - throw await parseBagPrintApiError(res, "刪除到期日 ZIP 品號"); + throw await parseBagPrintApiError(res, "刪除到期日 ZIP 品號", "Delete expiry-date ZIP item code", language); } } -export async function fetchOnPackSupportedCatalog(): Promise { +export async function fetchOnPackSupportedCatalog(language?: string): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/supported`; const res = await clientAuthFetch(url, { method: "GET" }); if (!res.ok) { - throw await parseBagPrintApiError(res, "讀取 OnPack 支援清單"); + throw await parseBagPrintApiError(res, "讀取 OnPack 支援清單", "Load OnPack support list", language); } return (await res.json()) as OnPackSupportedCatalogDto; } -export async function listOnPackTemplates(machine?: OnPackMachine): Promise { +export async function listOnPackTemplates(machine?: OnPackMachine, language?: string): Promise { const q = machine ? `?machine=${encodeURIComponent(machine)}` : ""; const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates${q}`; const res = await clientAuthFetch(url, { method: "GET" }); if (!res.ok) { - throw await parseBagPrintApiError(res, "讀取 OnPack 模板"); + throw await parseBagPrintApiError(res, "讀取 OnPack 模板", "Load OnPack template", language); } return (await res.json()) as OnPackTemplateFileDto[]; } @@ -362,6 +408,7 @@ export async function uploadOnPackTemplates( machine: OnPackMachine, itemCode: string, files: File[], + language?: string, ): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates`; const body = new FormData(); @@ -370,15 +417,15 @@ export async function uploadOnPackTemplates( files.forEach((f) => body.append("files", f)); const res = await clientAuthFetch(url, { method: "POST", body }); if (!res.ok) { - throw await parseBagPrintApiError(res, "上傳 OnPack 模板"); + throw await parseBagPrintApiError(res, "上傳 OnPack 模板", "Upload OnPack template", language); } return (await res.json()) as OnPackTemplateUploadResponse; } -export async function deleteOnPackTemplate(id: number): Promise { +export async function deleteOnPackTemplate(id: number, language?: string): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/${id}`; const res = await clientAuthFetch(url, { method: "DELETE" }); if (!res.ok && res.status !== 204) { - throw await parseBagPrintApiError(res, "刪除 OnPack 模板"); + throw await parseBagPrintApiError(res, "刪除 OnPack 模板", "Delete OnPack template", language); } } diff --git a/src/app/api/laserPrint/actions.ts b/src/app/api/laserPrint/actions.ts index ffde3afb..48fe1a27 100644 --- a/src/app/api/laserPrint/actions.ts +++ b/src/app/api/laserPrint/actions.ts @@ -2,6 +2,30 @@ import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; +import { I18N_COOKIE_NAME, normalizeAppLanguage } from "@/i18n/locale"; + +/** + * Optional `language` overrides detection. When omitted, use the page language + * (``), then the i18next cookie. Missing both stays Chinese. + */ +function resolveUiLanguage(language?: string | null): string { + if (typeof language === "string" && language.trim()) return language; + if (typeof document !== "undefined") { + const fromHtml = document.documentElement?.lang?.trim(); + if (fromHtml) return fromHtml; + const raw = document.cookie + .split("; ") + .find((part) => part.startsWith(`${I18N_COOKIE_NAME}=`)) + ?.slice(I18N_COOKIE_NAME.length + 1); + const fromCookie = normalizeAppLanguage(raw ? decodeURIComponent(raw) : null); + if (fromCookie) return fromCookie; + } + return "zh"; +} + +function uiText(zh: string, en: string, language?: string | null): string { + return resolveUiLanguage(language).toLowerCase().startsWith("en") ? en : zh; +} export interface JobOrderListItem { id: number; @@ -71,7 +95,7 @@ export interface LaserBag2SendResponse { * Uses server LASER_PRINT.itemCodes filter. Calls public GET /py/laser-job-orders (same as Python Bag2 /py/job-orders), * so it works without relying on authenticated /plastic routes. */ -export async function fetchLaserJobOrders(planStart: string): Promise { +export async function fetchLaserJobOrders(planStart: string, language?: string): Promise { const base = (NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, ""); if (!base) { throw new Error("NEXT_PUBLIC_API_URL is not set; cannot reach API."); @@ -83,19 +107,27 @@ export async function fetchLaserJobOrders(planStart: string): Promise ""); throw new Error( - `載入工單失敗(${res.status})${body ? `:${body.slice(0, 200)}` : ""}`, + uiText( + `載入工單失敗(${res.status})${body ? `:${body.slice(0, 200)}` : ""}`, + `Could not load job orders (${res.status})${body ? `: ${body.slice(0, 200)}` : ""}`, + language, + ), ); } return res.json() as Promise; } -export async function fetchLaserBag2Settings(): Promise { +export async function fetchLaserBag2Settings(language?: string): Promise { const base = (NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, ""); if (!base) { throw new Error("NEXT_PUBLIC_API_URL is not set."); @@ -106,11 +138,23 @@ export async function fetchLaserBag2Settings(): Promise { res = await clientAuthFetch(url, { method: "GET" }); } catch (e) { const msg = e instanceof Error ? e.message : String(e); - throw new Error(`無法連線至 ${url}:${msg}`); + throw new Error( + uiText( + `無法連線至 ${url}:${msg}`, + `Could not connect to ${url}: ${msg}`, + language, + ), + ); } if (!res.ok) { const body = await res.text().catch(() => ""); - throw new Error(`載入設定失敗(${res.status})${body ? body.slice(0, 200) : ""}`); + throw new Error( + uiText( + `載入設定失敗(${res.status})${body ? `:${body.slice(0, 200)}` : ""}`, + `Could not load settings (${res.status})${body ? `: ${body.slice(0, 200)}` : ""}`, + language, + ), + ); } return res.json() as Promise; } @@ -136,7 +180,7 @@ export function expiryDateForLaserSend(value: unknown): string | null { return null; } -function messageFromLaserSendBody(data: Record, status: number): string { +function messageFromLaserSendBody(data: Record, status: number, language?: string): string { const message = typeof data.message === "string" ? data.message.trim() : ""; if (message) return message; const detail = typeof data.detail === "string" ? data.detail.trim() : ""; @@ -144,11 +188,17 @@ function messageFromLaserSendBody(data: Record, status: number) const error = typeof data.error === "string" ? data.error.trim() : ""; if (error) return error; const traceId = typeof data.traceId === "string" ? data.traceId.trim() : ""; - if (traceId) return `送出失敗(traceId ${traceId})`; - return `送出失敗(HTTP ${status})`; + if (traceId) { + return uiText( + `送出失敗(traceId ${traceId})`, + `Send failed (traceId ${traceId})`, + language, + ); + } + return uiText(`送出失敗(HTTP ${status})`, `Send failed (HTTP ${status})`, language); } -export async function sendLaserBag2Job(body: LaserBag2SendRequest): Promise { +export async function sendLaserBag2Job(body: LaserBag2SendRequest, language?: string): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/print-laser-bag2`; const res = await clientAuthFetch(url, { method: "POST", @@ -160,12 +210,19 @@ export async function sendLaserBag2Job(body: LaserBag2SendRequest): Promise) : {}; } catch { - return { success: false, message: `送出失敗(HTTP ${res.status},無法解析回應)` }; + return { + success: false, + message: uiText( + `送出失敗(HTTP ${res.status},無法解析回應)`, + `Send failed (HTTP ${res.status}, response could not be read)`, + language, + ), + }; } if (!res.ok || data.success === false) { return { success: false, - message: messageFromLaserSendBody(data, res.status), + message: messageFromLaserSendBody(data, res.status, language), payloadSent: typeof data.payloadSent === "string" ? data.payloadSent : null, printerAck: typeof data.printerAck === "string" ? data.printerAck : null, receiveAcknowledged: Boolean(data.receiveAcknowledged), @@ -173,7 +230,9 @@ export async function sendLaserBag2Job(body: LaserBag2SendRequest): Promise { const session = await getServerSession(authOptions); const accessToken = session?.accessToken; @@ -72,6 +84,7 @@ export const serverFetch: typeof fetch = async (input, init) => { ...init, headers: { ...init?.headers, + "Accept-Language": requestLanguage(session?.locale), ...(accessToken ? { Authorization: `Bearer ${accessToken}`, diff --git a/src/components/BagPrint/BagPrintSearch.tsx b/src/components/BagPrint/BagPrintSearch.tsx index 8dd66c96..2f264a49 100644 --- a/src/components/BagPrint/BagPrintSearch.tsx +++ b/src/components/BagPrint/BagPrintSearch.tsx @@ -58,6 +58,9 @@ import { SessionWithTokens } from "@/config/authConfig"; import { AUTH, hasAbility } from "@/authorities"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; +import { useUiText } from "@/i18n/useUiText"; + +type UiText = (zh: string, en: string) => string; // Light blue theme (matching Python Bag1) const BG_TOP = "#E8F4FC"; @@ -69,10 +72,7 @@ const BG_STATUS_OK = "#90EE90"; const FG_STATUS_ERROR = "#B22222"; const FG_STATUS_OK = "#006400"; -const PRINTER_OPTIONS = [ - { value: "dataflex", label: "打袋機 DataFlex" }, - { value: "laser", label: "激光機" }, -]; +const PRINTER_OPTIONS = [{ value: "dataflex" }, { value: "laser" }]; const REFRESH_MS = 60 * 1000; const PRINTER_CHECK_MS = 60 * 1000; @@ -129,8 +129,8 @@ function expiryCodeSet(rows: OnPackExpiryItemCodeDto[]): Set { return new Set(rows.map((r) => r.itemCode.trim().toUpperCase()).filter(Boolean)); } -function daysLabel(value: number | null | undefined): string { - return value == null ? "未設" : String(value); +function daysLabel(value: number | null | undefined, unsetLabel: string): string { + return value == null ? unsetLabel : String(value); } type ExpirySortKey = @@ -156,7 +156,7 @@ function cmpDays(a: number | null | undefined, b: number | null | undefined): nu return av - bv; } -function skippedExpirySnackbar(okMessage: string, skipped: string[]): { +function skippedExpirySnackbar(okMessage: string, skipped: string[], L: UiText): { open: true; message: string; severity: "success" | "warning"; @@ -167,7 +167,10 @@ function skippedExpirySnackbar(okMessage: string, skipped: string[]): { } return { open: true, - message: `${okMessage}。以下品號沒有到期日,已略過不入 ZIP:${skipped.join("、")}。請到設定 → 物品預設保質期新增。`, + message: `${okMessage}${L( + `。以下品號沒有到期日,已略過不入 ZIP:${skipped.join("、")}。請到設定 → 物品預設保質期新增。`, + `. These item codes have no expiry date and were left out of the ZIP: ${skipped.join(", ")}. Add them under Settings → Default item shelf life.`, + )}`, severity: "warning", duration: 10000, }; @@ -206,7 +209,21 @@ function sortExpiryRows( }); } +export function BagPrintPageTitle() { + const L = useUiText(); + return ( + + + {L("打袋機", "Bag printer")} + + + ); +} + const BagPrintSearch: React.FC = () => { + const L = useUiText(); + const LRef = useRef(L); + LRef.current = L; const { data: session } = useSession() as { data: SessionWithTokens | null }; const abilities = session?.abilities ?? session?.user?.abilities ?? []; const canSeeOnPackAdmin = hasAbility(abilities, AUTH.ADMIN); @@ -243,7 +260,7 @@ const BagPrintSearch: React.FC = () => { }>({ open: false, message: "" }); const [settings, setSettings] = useState(DEFAULT_SETTINGS); const [printerConnected, setPrinterConnected] = useState(false); - const [printerMessage, setPrinterMessage] = useState("列印機未連接"); + const [printerMessage, setPrinterMessage] = useState(null); const [downloadingOnPack, setDownloadingOnPack] = useState(false); const [downloadingOnPackExp, setDownloadingOnPackExp] = useState(false); const [downloadingOnPackText, setDownloadingOnPackText] = useState(false); @@ -266,7 +283,7 @@ const BagPrintSearch: React.FC = () => { setConnected(true); if (fromUserChange) setSelectedId(null); } catch (e) { - setError(e instanceof Error ? e.message : "連接不到服務器"); + setError(e instanceof Error ? e.message : LRef.current("連接不到服務器", "Could not reach the server")); setConnected(false); setJobOrders([]); } finally { @@ -304,7 +321,7 @@ const BagPrintSearch: React.FC = () => { setPrinterMessage(result.message); } catch (e) { setPrinterConnected(false); - setPrinterMessage(e instanceof Error ? e.message : "列印機狀態檢查失敗"); + setPrinterMessage(e instanceof Error ? e.message : LRef.current("列印機狀態檢查失敗", "Could not check printer status")); } }, [printer, settings]); @@ -338,7 +355,14 @@ const BagPrintSearch: React.FC = () => { const batch = getBatch(jo); const itemCode = jo.itemCode || "—"; const itemName = jo.itemName || "—"; - setSnackbar({ open: true, message: `已點選:批次 ${batch} 品號 ${itemCode} ${itemName}`, severity: "info" }); + setSnackbar({ + open: true, + message: L( + `已點選:批次 ${batch} 品號 ${itemCode} ${itemName}`, + `Selected: lot ${batch}, item ${itemCode} ${itemName}`, + ), + severity: "info", + }); // Align with Bag2.py "click row -> ask bag count -> print" for DataFlex. if (printer === "dataflex") { @@ -352,12 +376,20 @@ const BagPrintSearch: React.FC = () => { const confirmPrintDataFlex = async () => { if (!printTarget) return; if (printer !== "dataflex") { - setSnackbar({ open: true, message: "此頁目前只支援打袋機 DataFlex 列印", severity: "error" }); + setSnackbar({ + open: true, + message: L("此頁目前只支援打袋機 DataFlex 列印", "This page only prints on the DataFlex bag printer"), + severity: "error", + }); return; } if (!printContinuous && printCount < 1) { - setSnackbar({ open: true, message: "請先按 +50、+10、+5 或 +1 選擇數量。", severity: "error" }); + setSnackbar({ + open: true, + message: L("請先按 +50、+10、+5 或 +1 選擇數量。", "Press +50, +10, +5, or +1 to choose a quantity first."), + severity: "error", + }); return; } @@ -366,7 +398,11 @@ const BagPrintSearch: React.FC = () => { const printerPort = Number(settings.dabag_port || 3008); if (!printerIp) { - setSnackbar({ open: true, message: "請先在設定中填寫打袋機 DataFlex 的 IP。", severity: "error" }); + setSnackbar({ + open: true, + message: L("請先在設定中填寫打袋機 DataFlex 的 IP。", "Enter the DataFlex bag printer IP in Settings first."), + severity: "error", + }); return; } @@ -394,18 +430,29 @@ const BagPrintSearch: React.FC = () => { const msg = await resp.text().catch(() => ""); setSnackbar({ open: true, - message: `DataFlex 列印失敗(狀態碼 ${resp.status})。${msg ? msg.slice(0, 120) : ""}`, + message: L( + `DataFlex 列印失敗(狀態碼 ${resp.status})。${msg ? msg.slice(0, 120) : ""}`, + `DataFlex print failed (status ${resp.status}).${msg ? ` ${msg.slice(0, 120)}` : ""}`, + ), severity: "error", }); return; } const batch = getBatch(printTarget); - const printedText = qty === -1 ? "連續 (C)" : `${qty}`; - setSnackbar({ open: true, message: `已送出列印:批次 ${batch} x ${printedText}`, severity: "success" }); + const printedText = qty === -1 ? L("連續 (C)", "Continuous (C)") : `${qty}`; + setSnackbar({ + open: true, + message: L(`已送出列印:批次 ${batch} x ${printedText}`, `Print sent: lot ${batch} x ${printedText}`), + severity: "success", + }); setPrintDialogOpen(false); } catch (e) { - setSnackbar({ open: true, message: e instanceof Error ? e.message : "DataFlex 列印失敗", severity: "error" }); + setSnackbar({ + open: true, + message: e instanceof Error ? e.message : L("DataFlex 列印失敗", "DataFlex print failed"), + severity: "error", + }); } finally { setPrinting(false); } @@ -416,7 +463,11 @@ const BagPrintSearch: React.FC = () => { const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); if (onPackJobOrders.length === 0) { - setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" }); + setSnackbar({ + open: true, + message: L("當日沒有可下載的 job order", "No job orders to download for this day"), + severity: "error", + }); return; } @@ -436,11 +487,15 @@ const BagPrintSearch: React.FC = () => { link.remove(); window.URL.revokeObjectURL(url); - setSnackbar({ open: true, message: "OnPack QR code ZIP 已下載", severity: "success" }); + setSnackbar({ + open: true, + message: L("OnPack QR code ZIP 已下載", "OnPack QR code ZIP downloaded"), + severity: "success", + }); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "下載 OnPack QR code 失敗", + message: e instanceof Error ? e.message : L("下載 OnPack QR code 失敗", "Could not download the OnPack QR code"), severity: "error", }); } finally { @@ -454,7 +509,11 @@ const BagPrintSearch: React.FC = () => { const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); if (onPackJobOrders.length === 0) { - setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" }); + setSnackbar({ + open: true, + message: L("當日沒有可下載的 job order", "No job orders to download for this day"), + severity: "error", + }); return; } @@ -475,11 +534,17 @@ const BagPrintSearch: React.FC = () => { link.remove(); window.URL.revokeObjectURL(url); - setSnackbar(skippedExpirySnackbar("OnPack QR(含到期日)ZIP 已下載", skippedWithoutExpiry)); + setSnackbar( + skippedExpirySnackbar( + L("OnPack QR(含到期日)ZIP 已下載", "OnPack QR (with expiry) ZIP downloaded"), + skippedWithoutExpiry, + L, + ), + ); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "下載 OnPack QR(含到期日)失敗", + message: e instanceof Error ? e.message : L("下載 OnPack QR(含到期日)失敗", "Could not download OnPack QR (with expiry)"), severity: "error", }); } finally { @@ -493,7 +558,11 @@ const BagPrintSearch: React.FC = () => { const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); if (onPackJobOrders.length === 0) { - setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" }); + setSnackbar({ + open: true, + message: L("當日沒有可下載的 job order", "No job orders to download for this day"), + severity: "error", + }); return; } @@ -513,11 +582,15 @@ const BagPrintSearch: React.FC = () => { link.remove(); window.URL.revokeObjectURL(url); - setSnackbar({ open: true, message: "OnPack 文字 ZIP 已下載", severity: "success" }); + setSnackbar({ + open: true, + message: L("OnPack 文字 ZIP 已下載", "OnPack text ZIP downloaded"), + severity: "success", + }); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "下載 OnPack 文字 失敗", + message: e instanceof Error ? e.message : L("下載 OnPack 文字 失敗", "Could not download OnPack text"), severity: "error", }); } finally { @@ -531,7 +604,11 @@ const BagPrintSearch: React.FC = () => { const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); if (onPackJobOrders.length === 0) { - setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" }); + setSnackbar({ + open: true, + message: L("當日沒有可下載的 job order", "No job orders to download for this day"), + severity: "error", + }); return; } @@ -552,11 +629,17 @@ const BagPrintSearch: React.FC = () => { link.remove(); window.URL.revokeObjectURL(url); - setSnackbar(skippedExpirySnackbar("OnPack 文字(含到期日)ZIP 已下載", skippedWithoutExpiry)); + setSnackbar( + skippedExpirySnackbar( + L("OnPack 文字(含到期日)ZIP 已下載", "OnPack text (with expiry) ZIP downloaded"), + skippedWithoutExpiry, + L, + ), + ); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "下載 OnPack 文字(含到期日)失敗", + message: e instanceof Error ? e.message : L("下載 OnPack 文字(含到期日)失敗", "Could not download OnPack text (with expiry)"), severity: "error", }); } finally { @@ -579,7 +662,7 @@ const BagPrintSearch: React.FC = () => { if (notify) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "讀取到期日 ZIP 品號失敗", + message: e instanceof Error ? e.message : LRef.current("讀取到期日 ZIP 品號失敗", "Could not load expiry ZIP item codes"), severity: "error", }); } @@ -619,19 +702,23 @@ const BagPrintSearch: React.FC = () => { if (expiryAddRef.current) return; const itemCode = expiryCodeInput.trim(); if (!itemCode) { - setSnackbar({ open: true, message: "請先填寫品號", severity: "error" }); + setSnackbar({ open: true, message: L("請先填寫品號", "Enter an item code first"), severity: "error" }); return; } expiryAddRef.current = true; try { await addOnPackExpiryCode(itemCode, "juice"); setExpiryCodeInput(""); - setSnackbar({ open: true, message: `已加入到期日 ZIP:${itemCode.toUpperCase()}`, severity: "success" }); + setSnackbar({ + open: true, + message: L(`已加入到期日 ZIP:${itemCode.toUpperCase()}`, `Added to the expiry ZIP: ${itemCode.toUpperCase()}`), + severity: "success", + }); await loadExpiryCodes(); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "新增到期日 ZIP 品號失敗", + message: e instanceof Error ? e.message : L("新增到期日 ZIP 品號失敗", "Could not add the expiry ZIP item code"), severity: "error", }); } finally { @@ -644,12 +731,16 @@ const BagPrintSearch: React.FC = () => { expiryDeleteRef.current = true; try { await deleteOnPackExpiryCode(itemCode, "juice"); - setSnackbar({ open: true, message: `已從到期日 ZIP 移除 ${itemCode}`, severity: "success" }); + setSnackbar({ + open: true, + message: L(`已從到期日 ZIP 移除 ${itemCode}`, `Removed ${itemCode} from the expiry ZIP`), + severity: "success", + }); await loadExpiryCodes(); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "刪除到期日 ZIP 品號失敗", + message: e instanceof Error ? e.message : L("刪除到期日 ZIP 品號失敗", "Could not remove the expiry ZIP item code"), severity: "error", }); } finally { @@ -675,11 +766,15 @@ const BagPrintSearch: React.FC = () => { printName: (nameDrafts[itemCode] ?? "").trim(), }); applyExpiryRow(updated); - setSnackbar({ open: true, message: `已儲存 ${itemCode} 列印名稱`, severity: "success" }); + setSnackbar({ + open: true, + message: L(`已儲存 ${itemCode} 列印名稱`, `Saved the print name for ${itemCode}`), + severity: "success", + }); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "儲存列印名稱失敗", + message: e instanceof Error ? e.message : L("儲存列印名稱失敗", "Could not save the print name"), severity: "error", }); } finally { @@ -699,13 +794,15 @@ const BagPrintSearch: React.FC = () => { applyExpiryRow(updated); setSnackbar({ open: true, - message: useMinus18 ? `已改用 ${itemCode} 的 -18 天數` : `已改用 ${itemCode} 的冷藏天數`, + message: useMinus18 + ? L(`已改用 ${itemCode} 的 -18 天數`, `Now using the −18°C days for ${itemCode}`) + : L(`已改用 ${itemCode} 的冷藏天數`, `Now using the chilled days for ${itemCode}`), severity: "success", }); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "更新保質期旗標失敗", + message: e instanceof Error ? e.message : L("更新保質期旗標失敗", "Could not update the shelf-life flag"), severity: "error", }); } finally { @@ -713,6 +810,8 @@ const BagPrintSearch: React.FC = () => { } }; + const printerStatusText = printerMessage ?? L("列印機未連接", "Printer not connected"); + const unsetDays = L("未設", "Not set"); const juiceExpiryCodeSet = expiryCodeSet(expiryCodes); const sortedExpiryCodes = useMemo( () => sortExpiryRows(expiryCodes, expirySortKey, expirySortDir), @@ -735,7 +834,7 @@ const BagPrintSearch: React.FC = () => { { InputLabelProps={{ shrink: true }} /> {canSeeOnPackAdmin && ( )} { fontWeight: 600, whiteSpace: "nowrap", }} - title={printerMessage} + title={printerStatusText} > - 列印機: + {L("列印機:", "Printer:")} - 列印機 - {PRINTER_OPTIONS.map((opt) => ( - {opt.label} + {opt.value === "dataflex" + ? L("打袋機 DataFlex", "DataFlex bag printer") + : L("激光機", "Laser printer")} ))} @@ -785,7 +886,7 @@ const BagPrintSearch: React.FC = () => { - {printerMessage} + {printerStatusText} {canSeeOnPackAdmin && ( )} @@ -863,7 +968,7 @@ const BagPrintSearch: React.FC = () => { ) : jobOrders.length === 0 ? ( - 當日無工單 + {L("當日無工單", "No job orders for this day")} ) : ( @@ -897,7 +1002,7 @@ const BagPrintSearch: React.FC = () => { {qtyStr !== "—" && ( - 數量:{qtyStr} + {L("數量:", "Qty:")}{qtyStr} )} @@ -912,7 +1017,7 @@ const BagPrintSearch: React.FC = () => { {juiceExpiryOk ? : null} - {lemonOk ? : null} + {lemonOk ? : null} @@ -929,7 +1034,7 @@ const BagPrintSearch: React.FC = () => { handleRowClick(jo); }} > - 列印 + {L("列印", "Print")} ); @@ -941,14 +1046,14 @@ const BagPrintSearch: React.FC = () => { {/* Print count dialog (DataFlex) */} (printing ? null : setPrintDialogOpen(false))} maxWidth="xs" fullWidth> - 打袋機 DataFlex 列印數量 + {L("打袋機 DataFlex 列印數量", "DataFlex bag printer quantity")} - 列印多少個袋? + {L("列印多少個袋?", "How many bags should be printed?")} - {printContinuous ? "連續 (C)" : `數量: ${printCount}`} + {printContinuous ? L("連續 (C)", "Continuous (C)") : L(`數量: ${printCount}`, `Qty: ${printCount}`)} {/* Settings dialog */} setSettingsOpen(false)} maxWidth="sm" fullWidth> - 設定 + {L("設定", "Settings")} - 打袋機 DataFlex + {L("打袋機 DataFlex", "DataFlex bag printer")} { fullWidth /> - 激光機 + {L("激光機", "Laser printer")} { - + @@ -1083,7 +1188,7 @@ const BagPrintSearch: React.FC = () => { scroll="paper" PaperProps={{ sx: { height: "90vh", maxHeight: "90vh" } }} > - OnPack 到期日 ZIP 品號 + {L("OnPack 到期日 ZIP 品號", "OnPack expiry ZIP item codes")} { > - OnPack QR({expiryCodes.length}) + {L(`OnPack QR(${expiryCodes.length})`, `OnPack QR (${expiryCodes.length})`)} - 「下載 OnPack QR(含到期日)」只用此清單。名稱印在 Product 行,過長可改短後按儲存。 - 點欄位標題可排序;冷藏/-18/列印第一次點擊會把「未設」排到最前。 + {L( + "「下載 OnPack QR(含到期日)」只用此清單。名稱印在 Product 行,過長可改短後按儲存。點欄位標題可排序;冷藏/-18/列印第一次點擊會把「未設」排到最前。", + "“Download OnPack QR (with expiry)” uses only this list. The name is printed on the Product line; shorten it and press Save if it is too long. Click a column title to sort. The first click on Chilled, −18, or Print puts “Not set” first.", + )} setExpiryCodeInput(e.target.value)} onKeyDown={(e) => { @@ -1116,7 +1223,7 @@ const BagPrintSearch: React.FC = () => { sx={{ minWidth: 180 }} /> @@ -1125,7 +1232,7 @@ const BagPrintSearch: React.FC = () => { ) : expiryCodes.length === 0 ? ( - 清單空白 + {L("清單空白", "The list is empty")} ) : (
@@ -1137,7 +1244,7 @@ const BagPrintSearch: React.FC = () => { direction={expirySortKey === "itemCode" ? expirySortDir : "asc"} onClick={() => onExpirySort("itemCode")} > - 品號 + {L("品號", "Item code")} @@ -1146,7 +1253,7 @@ const BagPrintSearch: React.FC = () => { direction={expirySortKey === "name" ? expirySortDir : "asc"} onClick={() => onExpirySort("name")} > - 中文名稱+單位 + {L("中文名稱+單位", "Name and unit")} @@ -1155,10 +1262,10 @@ const BagPrintSearch: React.FC = () => { direction={expirySortKey === "defaultDays" ? expirySortDir : "asc"} onClick={() => onExpirySort("defaultDays")} > - 冷藏 + {L("冷藏", "Chilled")} - 天 + {L("天", "days")} @@ -1170,7 +1277,7 @@ const BagPrintSearch: React.FC = () => { -18 - 天 + {L("天", "days")} @@ -1179,7 +1286,7 @@ const BagPrintSearch: React.FC = () => { direction={expirySortKey === "useMinus18" ? expirySortDir : "asc"} onClick={() => onExpirySort("useMinus18")} > - 用 -18 + {L("用 -18", "Use −18")} @@ -1188,14 +1295,14 @@ const BagPrintSearch: React.FC = () => { direction={expirySortKey === "effectiveDays" ? expirySortDir : "asc"} onClick={() => onExpirySort("effectiveDays")} > - 列印 + {L("列印", "Print")} - 天 + {L("天", "days")} - 操作 + {L("操作", "Actions")} @@ -1209,8 +1316,8 @@ const BagPrintSearch: React.FC = () => { const missingHint = hasShelf ? canUseMinus18 ? "" - : "此品號沒有 -18 天數" - : "未設定保質期,請到設定 → 物品預設保質期新增"; + : L("此品號沒有 -18 天數", "This item code has no −18°C days") + : L("未設定保質期,請到設定 → 物品預設保質期新增", "Shelf life is not set. Add it under Settings → Default item shelf life."); return ( @@ -1224,7 +1331,7 @@ const BagPrintSearch: React.FC = () => { onChange={(e) => setNameDrafts((prev) => ({ ...prev, [row.itemCode]: e.target.value })) } - placeholder={row.defaultPrintName || "例如 鄉村麵包"} + placeholder={row.defaultPrintName || L("例如 鄉村麵包", "e.g. country bread")} inputProps={{ maxLength: 255 }} fullWidth /> @@ -1234,7 +1341,7 @@ const BagPrintSearch: React.FC = () => { disabled={!nameDirty} onClick={() => void handleSaveExpiryPrintName(row.itemCode)} > - 儲存 + {L("儲存", "Save")} @@ -1244,7 +1351,7 @@ const BagPrintSearch: React.FC = () => { sx={{ fontWeight: 700, lineHeight: 1.2 }} color={row.defaultDays == null ? "warning.main" : "text.primary"} > - {daysLabel(row.defaultDays)} + {daysLabel(row.defaultDays, unsetDays)} @@ -1253,18 +1360,18 @@ const BagPrintSearch: React.FC = () => { sx={{ fontWeight: 700, lineHeight: 1.2 }} color={row.minus18Days == null ? "warning.main" : "text.primary"} > - {daysLabel(row.minus18Days)} + {daysLabel(row.minus18Days, unsetDays)} - + void handleToggleUseMinus18(row.itemCode, e.target.checked)} - inputProps={{ "aria-label": `${row.itemCode} 用 -18` }} + inputProps={{ "aria-label": L(`${row.itemCode} 用 -18`, `${row.itemCode} use −18`) }} /> @@ -1275,11 +1382,11 @@ const BagPrintSearch: React.FC = () => { sx={{ fontWeight: 800, lineHeight: 1.2 }} color={row.effectiveDays == null ? "warning.main" : "primary.main"} > - {daysLabel(row.effectiveDays)} + {daysLabel(row.effectiveDays, unsetDays)} {!hasShelf && ( - 未設定 + {L("未設定", "Not set")} )} @@ -1289,7 +1396,7 @@ const BagPrintSearch: React.FC = () => { color="error" onClick={() => void handleDeleteExpiryCode(row.itemCode)} > - 移除 + {L("移除", "Remove")} @@ -1300,11 +1407,11 @@ const BagPrintSearch: React.FC = () => { )} - 激光機到期日 ZIP 品號稍後加入。 + {L("激光機到期日 ZIP 品號稍後加入。", "Laser printer expiry ZIP item codes will be added later.")} - + diff --git a/src/components/ClientMonitor/ClientMonitorPage.tsx b/src/components/ClientMonitor/ClientMonitorPage.tsx index 120a510c..2b59bfad 100644 --- a/src/components/ClientMonitor/ClientMonitorPage.tsx +++ b/src/components/ClientMonitor/ClientMonitorPage.tsx @@ -41,6 +41,7 @@ import "dayjs/locale/zh-hk"; import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { useUiText } from "@/i18n/useUiText"; function formatApiDateTime(value: unknown): string { if (value == null) return "—"; @@ -120,25 +121,44 @@ type PrinterHistorySummary = { total: number; }; -const STATUS_LABEL: Record = { - online: { label: "在線", color: "success" }, - idle: { label: "閒置", color: "warning" }, - poor: { label: "連線差", color: "warning" }, - offline: { label: "離線", color: "error" }, +type UiText = (zh: string, en: string) => string; + +const STATUS_META: Record = { + online: { zh: "在線", en: "Online", color: "success" }, + idle: { zh: "閒置", en: "Idle", color: "warning" }, + poor: { zh: "連線差", en: "Poor connection", color: "warning" }, + offline: { zh: "離線", en: "Offline", color: "error" }, }; -const CLIENT_TYPE_LABEL: Record = { - tablet: "平板", - desktop: "電腦", - mobile: "手機", - unknown: "未知", +const CLIENT_TYPE_META: Record = { + tablet: { zh: "平板", en: "Tablet" }, + desktop: { zh: "電腦", en: "Computer" }, + mobile: { zh: "手機", en: "Phone" }, + unknown: { zh: "未知", en: "Unknown" }, }; -function formatAgo(seconds?: number): string { +function statusLabel(status: string, L: UiText) { + const st = STATUS_META[status] ?? STATUS_META.offline; + return { label: L(st.zh, st.en), color: st.color }; +} + +function clientTypeLabel(clientType: string | undefined, L: UiText) { + const meta = CLIENT_TYPE_META[clientType ?? ""]; + if (!meta) return clientType || "—"; + return L(meta.zh, meta.en); +} + +function formatAgo(seconds: number | undefined, L: UiText): string { if (seconds == null) return "—"; - if (seconds < 60) return `${seconds} 秒前`; - if (seconds < 3600) return `${Math.floor(seconds / 60)} 分鐘前`; - return `${Math.floor(seconds / 3600)} 小時前`; + if (seconds < 60) { + return L(`${seconds} 秒前`, seconds === 1 ? "1 second ago" : `${seconds} seconds ago`); + } + if (seconds < 3600) { + const minutes = Math.floor(seconds / 60); + return L(`${minutes} 分鐘前`, minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`); + } + const hours = Math.floor(seconds / 3600); + return L(`${hours} 小時前`, hours === 1 ? "1 hour ago" : `${hours} hours ago`); } const HISTORY_DATETIME_FORMAT = `${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`; @@ -157,6 +177,9 @@ function defaultHistoryTo(): Dayjs { } export default function ClientMonitorPage() { + const L = useUiText(); + const LRef = useRef(L); + LRef.current = L; const [tab, setTab] = useState<"live" | "history" | "printers">("live"); const [printerRefreshAt, setPrinterRefreshAt] = useState(0); @@ -208,7 +231,7 @@ export default function ClientMonitorPage() { setSummary(data.summary ?? null); } catch (e) { console.error("client monitor fetch", e); - setError("無法載入裝置狀態"); + setError(LRef.current("無法載入裝置狀態", "Could not load device status")); } finally { setLoading(false); fetchInFlightRef.current = false; @@ -220,12 +243,12 @@ export default function ClientMonitorPage() { const from = toApiDateTime(historyFrom); const to = toApiDateTime(historyTo); if (!from || !to) { - setHistoryError("請選擇開始與結束時間"); + setHistoryError(LRef.current("請選擇開始與結束時間", "Choose a start and end time")); return; } const fetchDevice = historyIncludeOffline || historyIncludePoor; if (!fetchDevice && !historyIncludePrinter) { - setHistoryError("請至少勾選「裝置」或「印表機離線」記錄"); + setHistoryError(LRef.current("請至少勾選「裝置」或「印表機離線」記錄", "Select at least Devices or Printer offline.")); return; } @@ -305,7 +328,7 @@ export default function ClientMonitorPage() { setHistoryQueried(true); } catch (e) { console.error("client monitor history", e); - setHistoryError(e instanceof Error ? e.message : "無法載入歷史記錄"); + setHistoryError(e instanceof Error ? e.message : LRef.current("無法載入歷史記錄", "Could not load history")); setHistoryRows([]); setHistorySummary(null); setPrinterHistoryRows([]); @@ -348,7 +371,7 @@ export default function ClientMonitorPage() { return (
)} - 重新整理 + {L("重新整理", "Refresh")} ) : tab === "history" ? ( ) : ( ) } @@ -396,46 +419,53 @@ export default function ClientMonitorPage() { onChange={(_, v: "live" | "history" | "printers") => setTab(v)} sx={{ borderBottom: 1, borderColor: "divider" }} > - - - + + + {tab === "live" && ( <> - 顯示最近 7 天內有心跳的裝置(平板、電腦等)。超過約 90 秒無心跳視為離線;超過 5 - 分鐘無頁面活動視為閒置。長期離線的舊紀錄會自動隱藏並清除。每 10 秒自動更新。 + {L( + "顯示最近 7 天內有心跳的裝置(平板、電腦等)。超過約 90 秒無心跳視為離線;超過 5 分鐘無頁面活動視為閒置。長期離線的舊紀錄會自動隱藏並清除。每 10 秒自動更新。", + "Shows devices with a heartbeat in the last 7 days (tablets, computers, and others). No heartbeat for about 90 seconds counts as offline. No page activity for 5 minutes counts as idle. Old records that stay offline are hidden and removed. Updates every 10 seconds.", + )} {offlineCount > 0 && ( - 有 {offlineCount} 台裝置可能已斷線或關閉應用,請檢查倉庫平板/網路。 + {L( + `有 ${offlineCount} 台裝置可能已斷線或關閉應用,請檢查倉庫平板/網路。`, + offlineCount === 1 + ? "1 device may be disconnected or the app may be closed. Check the warehouse tablets and network." + : `${offlineCount} devices may be disconnected or the app may be closed. Check the warehouse tablets and network.`, + )} )} - 裝置類型 + {L("裝置類型", "Device type")} {summary && ( - - - - - + + + + + )} @@ -446,15 +476,15 @@ export default function ClientMonitorPage() {
- 狀態 - 裝置 - 類型 - 帳號 + {L("狀態", "Status")} + {L("裝置", "Device")} + {L("類型", "Type")} + {L("帳號", "Account")} IP - 目前頁面 + {L("目前頁面", "Current page")} RTT (ms) - 最後心跳 - 最後活動 + {L("最後心跳", "Last heartbeat")} + {L("最後活動", "Last activity")} @@ -467,12 +497,12 @@ export default function ClientMonitorPage() { ) : rows.length === 0 ? ( - 目前沒有活躍裝置 + {L("目前沒有活躍裝置", "No active devices")} ) : ( rows.map((row) => { - const st = STATUS_LABEL[row.status] ?? STATUS_LABEL.offline; + const st = statusLabel(row.status, L); return ( @@ -487,9 +517,7 @@ export default function ClientMonitorPage() { - {CLIENT_TYPE_LABEL[row.clientType ?? ""] ?? - row.clientType ?? - "—"} + {clientTypeLabel(row.clientType, L)} {row.username ?? "—"} @@ -506,7 +534,7 @@ export default function ClientMonitorPage() { {row.rttMs != null ? row.rttMs : "—"} - {formatAgo(row.secondsSinceHeartbeat)} + {formatAgo(row.secondsSinceHeartbeat, L)} {row.lastHeartbeat && ( {formatApiDateTime(row.lastHeartbeat).slice(11)} @@ -544,9 +572,10 @@ export default function ClientMonitorPage() { {tab === "history" && ( <> - 查詢指定時間範圍內的裝置(平板/電腦)與印表機異常記錄(最長 - 30 天)。僅記錄離線與連線差事件;在線或閒置不會出現在此。切換到此分頁會自動查詢最近 - 7 天。 + {L( + "查詢指定時間範圍內的裝置(平板/電腦)與印表機異常記錄(最長 30 天)。僅記錄離線與連線差事件;在線或閒置不會出現在此。切換到此分頁會自動查詢最近 7 天。", + "Look up device (tablet or computer) and printer issues in a time range of up to 30 days. Only offline and poor-connection events are stored. Online or idle devices do not appear here. Opening this tab loads the last 7 days.", + )} - 裝置(平板/電腦,約每 30 秒檢測) + {L("裝置(平板/電腦,約每 30 秒檢測)", "Devices (tablet or computer, checked about every 30 seconds)")}
  • - 離線:超過約 90 秒沒有收到心跳(關閉分頁、關機、斷網太久,或應用被關閉)。 + {L( + "離線:超過約 90 秒沒有收到心跳(關閉分頁、關機、斷網太久,或應用被關閉)。", + "Offline: no heartbeat for about 90 seconds (the tab was closed, the device was powered off, the network was down for too long, or the app was closed).", + )}
  • - 連線差:仍有心跳,但符合以下任一情況: + {L("連線差:仍有心跳,但符合以下任一情況:", "Poor connection: heartbeats are still arriving, but any one of these is true:")}
    • - 瀏覽器回報目前無網路(navigator.onLine 為 false,例如 Wi‑Fi 已斷但分頁尚未關閉) + {L( + "瀏覽器回報目前無網路(navigator.onLine 為 false,例如 Wi‑Fi 已斷但分頁尚未關閉)", + "The browser reports no network (navigator.onLine is false, for example Wi‑Fi dropped but the tab is still open).", + )}
    • - 至伺服器的往返時間 RTT ≥ 3 秒(ping API 過慢) + {L( + "至伺服器的往返時間 RTT ≥ 3 秒(ping API 過慢)", + "Round-trip time to the server is 3 seconds or more (the ping API is too slow).", + )}
    • - 無法完成 ping(請求失敗或逾時,RTT 欄可能為「—」) + {L( + "無法完成 ping(請求失敗或逾時,RTT 欄可能為「—」)", + "The ping could not finish (the request failed or timed out). RTT may show “—”.", + )}
- 印表機(約每 2 分鐘掃描「設定」中的印表機 IP:Port) + {L( + "印表機(約每 2 分鐘掃描「設定」中的印表機 IP:Port)", + "Printers (the printer IP and port in Settings are scanned about every 2 minutes)", + )}
  • - 離線/無法連線:伺服器無法在約 3 秒內以 TCP - 連上該印表機的 IP 與 Port(預設 9100);持續離線時約每{" "} - 5 分鐘記錄一次。 + {L( + "離線/無法連線:伺服器無法在約 3 秒內以 TCP 連上該印表機的 IP 與 Port(預設 9100);持續離線時約每 5 分鐘記錄一次。", + "Offline / unreachable: the server cannot open a TCP connection to the printer IP and port (default 9100) within about 3 seconds. While it stays offline, a record is written about every 5 minutes.", + )}
- 裝置「連線差」為網路慢或不穩但仍有心跳;「離線」為長時間無心跳。印表機記錄僅包含無法連線事件,與即時「印表機」分頁的警告清單相同來源。 + {L( + "裝置「連線差」為網路慢或不穩但仍有心跳;「離線」為長時間無心跳。印表機記錄僅包含無法連線事件,與即時「印表機」分頁的警告清單相同來源。", + "For devices, poor connection means the network is slow or unstable but heartbeats are still arriving. Offline means there has been no heartbeat for a long time. Printer records include only unreachable events, from the same source as the warnings on the live Printers tab.", + )}
- + - 裝置類型 + {L("裝置類型", "Device type")} @@ -649,7 +699,7 @@ export default function ClientMonitorPage() { onChange={(e) => setHistoryIncludeOffline(e.target.checked)} /> } - label="離線" + label={L("離線", "Offline")} /> setHistoryIncludePoor(e.target.checked)} /> } - label="連線差" + label={L("連線差", "Poor connection")} /> setHistoryIncludePrinter(e.target.checked)} /> } - label="印表機離線" + label={L("印表機離線", "Printer offline")} /> @@ -679,20 +729,20 @@ export default function ClientMonitorPage() { {historySummary && ( <> - - - - - + + + + + )} {printerHistorySummary && ( <> - + )} @@ -704,7 +754,7 @@ export default function ClientMonitorPage() { {(historyIncludeOffline || historyIncludePoor) && ( <> - 裝置記錄 + {L("裝置記錄", "Device records")} - 時間 - 狀態 - 裝置 - 類型 - 帳號 + {L("時間", "Time")} + {L("狀態", "Status")} + {L("裝置", "Device")} + {L("類型", "Type")} + {L("帳號", "Account")} IP - 頁面 + {L("頁面", "Page")} RTT (ms) @@ -742,12 +792,12 @@ export default function ClientMonitorPage() { ) : historyRows.length === 0 ? ( - 此時間範圍內沒有裝置離線或連線差記錄 + {L("此時間範圍內沒有裝置離線或連線差記錄", "No offline or poor-connection records for devices in this period")} ) : ( historyRows.map((row) => { - const st = STATUS_LABEL[row.status] ?? STATUS_LABEL.offline; + const st = statusLabel(row.status, L); return ( {formatApiDateTime(row.recordedAt)} @@ -763,9 +813,7 @@ export default function ClientMonitorPage() {
- {CLIENT_TYPE_LABEL[row.clientType ?? ""] ?? - row.clientType ?? - "—"} + {clientTypeLabel(row.clientType, L)} {row.username ?? "—"} {formatClientIpDisplay(row.clientIp)} @@ -789,13 +837,13 @@ export default function ClientMonitorPage() { {historyIncludePrinter && ( <> - 印表機離線記錄 + {L("印表機離線記錄", "Printer offline records")} @@ -803,11 +851,11 @@ export default function ClientMonitorPage() {
- 時間 - 印表機 - 類型 + {L("時間", "Time")} + {L("印表機", "Printer")} + {L("類型", "Type")} IP:Port - 錯誤 + {L("錯誤", "Error")} @@ -820,7 +868,7 @@ export default function ClientMonitorPage() { ) : printerHistoryRows.length === 0 ? ( - 此時間範圍內沒有印表機離線記錄 + {L("此時間範圍內沒有印表機離線記錄", "No printer offline records in this period")} ) : ( @@ -851,7 +899,7 @@ export default function ClientMonitorPage() { - {row.errorMessage ?? "無法連線"} + {row.errorMessage ?? L("無法連線", "Could not connect")} diff --git a/src/components/ClientMonitor/DeviceConnectivityHistoryChart.tsx b/src/components/ClientMonitor/DeviceConnectivityHistoryChart.tsx index 87e27c65..296e373b 100644 --- a/src/components/ClientMonitor/DeviceConnectivityHistoryChart.tsx +++ b/src/components/ClientMonitor/DeviceConnectivityHistoryChart.tsx @@ -2,6 +2,8 @@ import React, { useMemo } from "react"; import { Alert, Box, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import type { ApexOptions } from "apexcharts"; import SafeApexCharts from "@/components/charts/SafeApexCharts"; import { @@ -25,11 +27,15 @@ export default function DeviceConnectivityHistoryChart({ events, fromLocal, toLocal, - title = "連線狀況圖", + title, subtitle, variant = "device", queried = true, }: Props) { + const { i18n } = useTranslation(); + const L = useUiText(); + const lang = i18n.resolvedLanguage || i18n.language || ""; + const chartTitle = title ?? L("連線狀況圖", "Connectivity"); const buckets = useMemo( () => buildConnectivityChartBuckets(events, fromLocal, toLocal), [events, fromLocal, toLocal] @@ -45,13 +51,14 @@ export default function DeviceConnectivityHistoryChart({ const barSeries = useMemo(() => { if (variant === "printer") { - return [{ name: "離線", data: buckets?.offlineCounts ?? [] }]; + return [{ name: L("離線", "Offline"), data: buckets?.offlineCounts ?? [] }]; } return [ - { name: "離線", data: buckets?.offlineCounts ?? [] }, - { name: "連線差", data: buckets?.poorCounts ?? [] }, + { name: L("離線", "Offline"), data: buckets?.offlineCounts ?? [] }, + { name: L("連線差", "Poor connection"), data: buckets?.poorCounts ?? [] }, ]; - }, [variant, buckets]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [variant, buckets, lang]); const options: ApexOptions = useMemo( () => ({ @@ -77,12 +84,12 @@ export default function DeviceConnectivityHistoryChart({ rotateAlways: buckets ? buckets.categories.length > 12 : false, hideOverlappingLabels: true, }, - title: { text: "時間" }, + title: { text: L("時間", "Time") }, }, yaxis: { min: 0, forceNiceScale: true, - title: { text: "事件次數" }, + title: { text: L("事件次數", "Events") }, labels: { formatter: (v: number) => String(Math.round(v)), }, @@ -97,7 +104,9 @@ export default function DeviceConnectivityHistoryChart({ }, fill: { opacity: 0.92 }, }), - [buckets, variant] + // lang tracks the UI language; L is a new function every render. + // eslint-disable-next-line react-hooks/exhaustive-deps + [buckets, variant, lang] ); const lineOptions: ApexOptions = useMemo( @@ -126,7 +135,9 @@ export default function DeviceConnectivityHistoryChart({ min: 0, forceNiceScale: true, title: { - text: variant === "printer" ? "異常印表機數" : "異常裝置數", + text: variant === "printer" + ? L("異常印表機數", "Printers with issues") + : L("異常裝置數", "Devices with issues"), }, labels: { formatter: (v: number) => String(Math.round(v)), @@ -134,22 +145,38 @@ export default function DeviceConnectivityHistoryChart({ }, tooltip: { y: { - formatter: (v: number) => `${Math.round(v)} 台`, + formatter: (v: number) => { + const n = Math.round(v); + if (variant === "printer") { + return L(`${n} 台`, n === 1 ? "1 printer" : `${n} printers`); + } + return L(`${n} 台`, n === 1 ? "1 device" : `${n} devices`); + }, }, }, }), - [buckets, variant] + // eslint-disable-next-line react-hooks/exhaustive-deps + [buckets, variant, lang] ); const defaultSubtitle = variant === "printer" - ? "每段時間內印表機 TCP 連線失敗次數(約每 2 分鐘掃描一次)。" - : "每段時間內離線/連線差事件次數。柱狀愈低愈好;藍線為該時段有異常的裝置數。"; + ? L( + "每段時間內印表機 TCP 連線失敗次數(約每 2 分鐘掃描一次)。", + "TCP connection failures per time bucket (scanned about every 2 minutes).", + ) + : L( + "每段時間內離線/連線差事件次數。柱狀愈低愈好;藍線為該時段有異常的裝置數。", + "Offline and poor-connection events per time bucket. Shorter bars are better. The blue line is how many devices had an issue in that bucket.", + ); if (!queried) { return ( - 選擇時間範圍後按「查詢」,或切換到此分頁時會自動載入。 + {L( + "選擇時間範圍後按「查詢」,或切換到此分頁時會自動載入。", + "Choose a time range and press Search, or open this tab to load automatically.", + )} ); } @@ -157,7 +184,10 @@ export default function DeviceConnectivityHistoryChart({ if (!buckets || buckets.categories.length === 0) { return ( - 無法建立圖表:請確認開始時間早於結束時間。 + {L( + "無法建立圖表:請確認開始時間早於結束時間。", + "Could not build the chart. Check that the start time is before the end time.", + )} ); } @@ -177,10 +207,14 @@ export default function DeviceConnectivityHistoryChart({
- {title} + {chartTitle} - {subtitle ?? `每 ${buckets.bucketMinutes} 分鐘統計一次。${defaultSubtitle}`} + {subtitle ?? + `${L( + `每 ${buckets.bucketMinutes} 分鐘統計一次。`, + `Totals every ${buckets.bucketMinutes} minutes. `, + )}${defaultSubtitle}`}
- {noIncidents ? "此區間無異常" : `穩定時段 ${healthPct}%`} + {noIncidents + ? L("此區間無異常", "No issues in this period") + : L(`穩定時段 ${healthPct}%`, `Stable time ${healthPct}%`)}
{noIncidents && ( {variant === "printer" - ? "此時間範圍內沒有印表機離線記錄。" - : "此時間範圍內沒有裝置離線或連線差記錄(閒置/在線不會寫入歷史)。"} + ? L("此時間範圍內沒有印表機離線記錄。", "No printer offline records in this period.") + : L( + "此時間範圍內沒有裝置離線或連線差記錄(閒置/在線不會寫入歷史)。", + "No device offline or poor-connection records in this period. Idle and online devices are not stored in history.", + )} )} @@ -213,28 +252,41 @@ export default function DeviceConnectivityHistoryChart({ height={280} series={barSeries} options={options} - chartRevision={`bar-${chartRevision}-${variant}`} + chartRevision={`bar-${chartRevision}-${variant}-${lang}`} allowZeroSeries /> {variant === "printer" ? ( <> - 區間內:離線記錄 {totalOffline} 次;單段最多 {maxAffected} 台印表機同時無法連線。 + {L( + `區間內:離線記錄 ${totalOffline} 次;單段最多 ${maxAffected} 台印表機同時無法連線。`, + `In this period: ${totalOffline} offline record${totalOffline === 1 ? "" : "s"}. At most ${maxAffected} printer${maxAffected === 1 ? "" : "s"} unreachable in one time bucket.`, + )} ) : ( <> - 區間內:離線事件 {totalOffline} 次、連線差 {totalPoor} 次;單段最多 {maxAffected}{" "} - 台裝置同時異常。 + {L( + `區間內:離線事件 ${totalOffline} 次、連線差 ${totalPoor} 次;單段最多 ${maxAffected} 台裝置同時異常。`, + `In this period: ${totalOffline} offline event${totalOffline === 1 ? "" : "s"} and ${totalPoor} poor-connection event${totalPoor === 1 ? "" : "s"}. At most ${maxAffected} device${maxAffected === 1 ? "" : "s"} affected in one time bucket.`, + )} )} diff --git a/src/components/ClientMonitor/LabelPrinterMonitorPanel.tsx b/src/components/ClientMonitor/LabelPrinterMonitorPanel.tsx index 88238a1a..2bb4aa0d 100644 --- a/src/components/ClientMonitor/LabelPrinterMonitorPanel.tsx +++ b/src/components/ClientMonitor/LabelPrinterMonitorPanel.tsx @@ -30,6 +30,7 @@ import "dayjs/locale/zh-hk"; import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { useUiText } from "@/i18n/useUiText"; function formatApiDateTime(value: unknown): string { if (value == null) return "—"; @@ -100,16 +101,23 @@ function defaultStatsTo(): Dayjs { return dayjs(); } -const STATUS_LABEL: Record< +type UiText = (zh: string, en: string) => string; + +const STATUS_META: Record< string, - { label: string; color: "success" | "warning" | "error" | "default" } + { zh: string; en: string; color: "success" | "warning" | "error" | "default" } > = { - online: { label: "連線正常", color: "success" }, - offline: { label: "離線", color: "error" }, - unconfigured: { label: "未設定 IP", color: "warning" }, - unchecked: { label: "未檢查", color: "default" }, + online: { zh: "連線正常", en: "Connected", color: "success" }, + offline: { zh: "離線", en: "Offline", color: "error" }, + unconfigured: { zh: "未設定 IP", en: "IP not set", color: "warning" }, + unchecked: { zh: "未檢查", en: "Not checked", color: "default" }, }; +function statusLabel(status: string, L: UiText) { + const st = STATUS_META[status] ?? STATUS_META.unchecked; + return { label: L(st.zh, st.en), color: st.color }; +} + type Props = { active: boolean; refreshAt?: number; @@ -119,6 +127,9 @@ export default function LabelPrinterMonitorPanel({ active, refreshAt = 0, }: Props) { + const L = useUiText(); + const LRef = useRef(L); + LRef.current = L; const [printers, setPrinters] = useState([]); const [summary, setSummary] = useState(null); const [odometerStats, setOdometerStats] = useState( @@ -138,15 +149,20 @@ export default function LabelPrinterMonitorPanel({ const fromApi = toApiDateTime(from); const toApi = toApiDateTime(to); if (!fromApi || !toApi) { - setStatsError("請選擇開始與結束時間"); + setStatsError(LRef.current("請選擇開始與結束時間", "Choose a start and end time")); return; } if (from.isAfter(to)) { - setStatsError("開始時間必須早於結束時間"); + setStatsError(LRef.current("開始時間必須早於結束時間", "Start time must be before end time")); return; } if (to.diff(from, "day") > STATS_MAX_RANGE_DAYS) { - setStatsError(`查詢區間不可超過 ${STATS_MAX_RANGE_DAYS} 天`); + setStatsError( + LRef.current( + `查詢區間不可超過 ${STATS_MAX_RANGE_DAYS} 天`, + `The range cannot be longer than ${STATS_MAX_RANGE_DAYS} days`, + ), + ); return; } @@ -169,7 +185,7 @@ export default function LabelPrinterMonitorPanel({ } catch (e) { console.error("label printer odometer stats", e); setStatsError( - e instanceof Error ? e.message : "無法載入 Zebra 里程表區間統計", + e instanceof Error ? e.message : LRef.current("無法載入 Zebra 里程表區間統計", "Could not load Zebra odometer statistics"), ); setOdometerStats(null); } finally { @@ -191,7 +207,10 @@ export default function LabelPrinterMonitorPanel({ if (checkRes.status === 401 || checkRes.status === 403) return; if (checkRes.status === 404) { throw new Error( - "標籤印表機監控 API 未啟用,請重新啟動後端並確認已部署最新版本" + LRef.current( + "標籤印表機監控 API 未啟用,請重新啟動後端並確認已部署最新版本", + "The label printer monitor API is not enabled. Restart the backend and confirm the latest version is deployed.", + ) ); } if (!checkRes.ok) { @@ -203,7 +222,7 @@ export default function LabelPrinterMonitorPanel({ } catch (e) { console.error("label printer monitor check", e); setError( - e instanceof Error ? e.message : "無法檢查標籤印表機狀態" + e instanceof Error ? e.message : LRef.current("無法檢查標籤印表機狀態", "Could not check label printer status") ); } finally { setLoading(false); @@ -238,31 +257,38 @@ export default function LabelPrinterMonitorPanel({ return (
- 標籤印表機監控(In Development) + {L("標籤印表機監控(In Development)", "Label printer monitor (in development)")} - 監控「印表機」設定中 type=Label 的設備:TCP 連線檢測,Zebra - 機型另讀取內建里程表(odometer)。 + {L( + "監控「印表機」設定中 type=Label 的設備:TCP 連線檢測,Zebra 機型另讀取內建里程表(odometer)。", + "Monitors printers in Printer settings with type Label: a TCP connectivity check. Zebra models also read the built-in odometer.", + )} {offlineCount > 0 && ( - 有 {offlineCount} 台標籤印表機無法連線,請檢查電源、網路或 IP/Port。 + {L( + `有 ${offlineCount} 台標籤印表機無法連線,請檢查電源、網路或 IP/Port。`, + offlineCount === 1 + ? "1 label printer could not be reached. Check the power, network, or IP/port." + : `${offlineCount} label printers could not be reached. Check the power, network, or IP/port.`, + )} )} {summary && ( - - - + + + {summary.unchecked > 0 && ( - + )} )} @@ -270,15 +296,15 @@ export default function LabelPrinterMonitorPanel({ {error && {error}} - 標籤印表機狀態 + {L("標籤印表機狀態", "Label printer status")}
- 狀態 - 印表機 - 品牌 + {L("狀態", "Status")} + {L("印表機", "Printer")} + {L("品牌", "Brand")} IP:Port - 累計里程 - + {L("累計里程", "Total count")} + @@ -306,16 +332,16 @@ export default function LabelPrinterMonitorPanel({ gap: 0.5, }} > - 自上次檢查 - + {L("自上次檢查", "Since last check")} + - 延遲 (ms) - 最後檢查 + {L("延遲 (ms)", "Latency (ms)")} + {L("最後檢查", "Last check")} @@ -328,12 +354,12 @@ export default function LabelPrinterMonitorPanel({ ) : printers.length === 0 ? ( - 沒有 Label 類型印表機 + {L("沒有 Label 類型印表機", "No printers of type Label")} ) : ( printers.map((row) => { - const st = STATUS_LABEL[row.status] ?? STATUS_LABEL.unchecked; + const st = statusLabel(row.status, L); const showOdometer = isZebra(row.brand); return ( @@ -388,16 +414,18 @@ export default function LabelPrinterMonitorPanel({ - Zebra 里程表區間印量(估算) + {L("Zebra 里程表區間印量(估算)", "Zebra odometer count for the period (estimated)")} - 依監控掃描寫入的 odometer 快照估算(最長 {STATS_MAX_RANGE_DAYS} 天)。非 - Zebra 機型不提供區間印量。 + {L( + `依監控掃描寫入的 odometer 快照估算(最長 ${STATS_MAX_RANGE_DAYS} 天)。非 Zebra 機型不提供區間印量。`, + `Estimated from odometer snapshots written by the monitor (up to ${STATS_MAX_RANGE_DAYS} days). Non-Zebra models do not have a period count.`, + )} - + void fetchOdometerStats(statsFrom, statsTo)} sx={{ mb: 0.25 }} > - 查詢統計 + {L("查詢統計", "Run query")} @@ -443,10 +471,10 @@ export default function LabelPrinterMonitorPanel({
- 印表機 - 區間起點 - 區間終點 - 區間印量(估算) + {L("印表機", "Printer")} + {L("區間起點", "Period start")} + {L("區間終點", "Period end")} + {L("區間印量(估算)", "Labels in period (estimated)")} @@ -459,7 +487,7 @@ export default function LabelPrinterMonitorPanel({ ) : !odometerStats?.printers?.length ? ( - 此區間內尚無 Zebra 里程表記錄 + {L("此區間內尚無 Zebra 里程表記錄", "No Zebra odometer records in this period")} ) : ( diff --git a/src/components/ClientMonitor/PrinterMonitorTab.tsx b/src/components/ClientMonitor/PrinterMonitorTab.tsx index 6c2fbb5e..fbb7c060 100644 --- a/src/components/ClientMonitor/PrinterMonitorTab.tsx +++ b/src/components/ClientMonitor/PrinterMonitorTab.tsx @@ -18,6 +18,7 @@ import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; import { arrayToDateTimeString } from "@/app/utils/formatUtil"; import dayjs from "dayjs"; +import { useUiText } from "@/i18n/useUiText"; function formatApiDateTime(value: unknown): string { if (value == null) return "—"; @@ -50,16 +51,23 @@ type PrinterSummary = { unchecked: number; }; -const PRINTER_STATUS_LABEL: Record< +type UiText = (zh: string, en: string) => string; + +const PRINTER_STATUS_META: Record< string, - { label: string; color: "success" | "warning" | "error" | "default" } + { zh: string; en: string; color: "success" | "warning" | "error" | "default" } > = { - online: { label: "連線正常", color: "success" }, - offline: { label: "離線", color: "error" }, - unconfigured: { label: "未設定 IP", color: "warning" }, - unchecked: { label: "未檢查", color: "default" }, + online: { zh: "連線正常", en: "Connected", color: "success" }, + offline: { zh: "離線", en: "Offline", color: "error" }, + unconfigured: { zh: "未設定 IP", en: "IP not set", color: "warning" }, + unchecked: { zh: "未檢查", en: "Not checked", color: "default" }, }; +function printerStatusLabel(status: string, L: UiText) { + const st = PRINTER_STATUS_META[status] ?? PRINTER_STATUS_META.unchecked; + return { label: L(st.zh, st.en), color: st.color }; +} + type Props = { active: boolean; /** Increment from parent to trigger an immediate connectivity check. */ @@ -67,6 +75,9 @@ type Props = { }; export default function PrinterMonitorTab({ active, refreshAt = 0 }: Props) { + const L = useUiText(); + const LRef = useRef(L); + LRef.current = L; const [printers, setPrinters] = useState([]); const [warnings, setWarnings] = useState([]); const [summary, setSummary] = useState(null); @@ -87,7 +98,10 @@ export default function PrinterMonitorTab({ active, refreshAt = 0 }: Props) { if (response.status === 401 || response.status === 403) return; if (response.status === 404) { throw new Error( - "印表機監控 API 未啟用,請重新啟動後端並確認已部署最新版本" + LRef.current( + "印表機監控 API 未啟用,請重新啟動後端並確認已部署最新版本", + "The printer monitor API is not enabled. Restart the backend and confirm the latest version is deployed.", + ) ); } if (!response.ok) { @@ -100,7 +114,7 @@ export default function PrinterMonitorTab({ active, refreshAt = 0 }: Props) { } catch (e) { console.error("printer monitor check", e); setError( - e instanceof Error ? e.message : "無法檢查印表機連線" + e instanceof Error ? e.message : LRef.current("無法檢查印表機連線", "Could not check printer connectivity") ); } finally { setLoading(false); @@ -125,24 +139,31 @@ export default function PrinterMonitorTab({ active, refreshAt = 0 }: Props) { return (
- 監控「印表機」設定中所有未刪除的印表機,以 TCP 連線至 IP:Port(預設 9100)檢測是否可連線。每 - 2 分鐘自動檢查;離線印表機會顯示於下方警告清單並寫入事件記錄。 + {L( + "監控「印表機」設定中所有未刪除的印表機,以 TCP 連線至 IP:Port(預設 9100)檢測是否可連線。每 2 分鐘自動檢查;離線印表機會顯示於下方警告清單並寫入事件記錄。", + "Monitors every printer that is not deleted in Printer settings by opening a TCP connection to the IP and port (default 9100). Checks automatically every 2 minutes. Offline printers appear in the warning list below and are written to the event log.", + )} {offlineCount > 0 && ( - 有 {offlineCount} 台印表機無法連線,請檢查電源、網路或 IP/Port 設定。 + {L( + `有 ${offlineCount} 台印表機無法連線,請檢查電源、網路或 IP/Port 設定。`, + offlineCount === 1 + ? "1 printer could not be reached. Check the power, network, or IP/port settings." + : `${offlineCount} printers could not be reached. Check the power, network, or IP/port settings.`, + )} )} {summary && ( - - - - + + + + {summary.unchecked > 0 && ( - + )} )} @@ -152,17 +173,17 @@ export default function PrinterMonitorTab({ active, refreshAt = 0 }: Props) { {warnings.length > 0 && (
- 離線警告清單 + {L("離線警告清單", "Offline warnings")}
- 印表機 - 類型 + {L("印表機", "Printer")} + {L("類型", "Type")} IP:Port - 錯誤 - 最後檢查 + {L("錯誤", "Error")} + {L("最後檢查", "Last check")} @@ -186,7 +207,7 @@ export default function PrinterMonitorTab({ active, refreshAt = 0 }: Props) { - {row.errorMessage ?? "無法連線"} + {row.errorMessage ?? L("無法連線", "Could not connect")} {formatApiDateTime(row.lastCheckAt)} @@ -199,18 +220,18 @@ export default function PrinterMonitorTab({ active, refreshAt = 0 }: Props) { )} - 全部印表機 + {L("全部印表機", "All printers")}
- 狀態 - 印表機 - 類型 + {L("狀態", "Status")} + {L("印表機", "Printer")} + {L("類型", "Type")} IP:Port - 延遲 (ms) - 最後檢查 + {L("延遲 (ms)", "Latency (ms)")} + {L("最後檢查", "Last check")} @@ -223,13 +244,12 @@ export default function PrinterMonitorTab({ active, refreshAt = 0 }: Props) { ) : printers.length === 0 ? ( - 沒有印表機資料 + {L("沒有印表機資料", "No printer data")} ) : ( printers.map((row) => { - const st = - PRINTER_STATUS_LABEL[row.status] ?? PRINTER_STATUS_LABEL.unchecked; + const st = printerStatusLabel(row.status, L); return ( diff --git a/src/components/DoSearch/DoSearch.tsx b/src/components/DoSearch/DoSearch.tsx index da8114da..01a7fa7e 100644 --- a/src/components/DoSearch/DoSearch.tsx +++ b/src/components/DoSearch/DoSearch.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors"; import { DoResult } from "@/app/api/do"; import { @@ -94,6 +95,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea const { t } = useTranslation("do"); + const L = useUiText(); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; @@ -276,8 +278,8 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea flex: 1, renderCell: (params) => { const v = params.row.truckLanceCode; - if (v == null) return "車線-X"; - if (typeof v === "string" && v.trim() === "") return "車線-X"; + if (v == null) return L("車線-X", "Truck X"); + if (typeof v === "string" && v.trim() === "") return L("車線-X", "Truck X"); return v; } }, diff --git a/src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx b/src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx index 2334b7fc..ba291305 100644 --- a/src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx +++ b/src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors"; import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from "@mui/material"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -52,6 +53,7 @@ const WorkbenchFloorLanePanel: React.FC = ({ onLanePanelPrefsChange, }) => { const { t } = useTranslation("pickOrder"); + const L = useUiText(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; const [selectedStore, setSelectedStore] = useState(FLOOR_A); @@ -568,7 +570,7 @@ const WorkbenchFloorLanePanel: React.FC = ({ }} > {/*{`${selectedDeliveryDateYmd} (${defaultTruckCount})`}*/} - {t("車線-X")}({defaultTruckCount}) + {L("車線-X", "Truck X")}({defaultTruckCount}) )} @@ -672,7 +674,7 @@ const WorkbenchFloorLanePanel: React.FC = ({ setModalOpen(true); }} > - {`${t("車線-X")} (${beforeTodayTruckXCount})`} + {`${L("車線-X", "Truck X")} (${beforeTodayTruckXCount})`} )} @@ -698,7 +700,7 @@ const WorkbenchFloorLanePanel: React.FC = ({ const is4F = floorKey === FLOOR_B_KEY; const labelCore = isTruckX - ? `${t("車線-X")}${sid ? ` · ${sid}` : ""} (${lane.unassigned}/${lane.total})` + ? `${L("車線-X", "Truck X")}${sid ? ` · ${sid}` : ""} (${lane.unassigned}/${lane.total})` : is4F && lane.loadingSequence != null ? `${t("Loading sequence n", { n: lane.loadingSequence })} (${lane.unassigned}/${lane.total})` : `${dep ? `${dep} ` : ""}${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`; diff --git a/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx b/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx index d12e34e4..935ef13d 100644 --- a/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx +++ b/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx @@ -1,6 +1,7 @@ "use client"; -import { FLOOR_A_KEY, FLOOR_B_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors"; +import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; +import { useUiText } from "@/i18n/useUiText"; import { Box, Button, @@ -570,6 +571,7 @@ const WorkbenchGoodPickExecutionDetail: React.FC = ({ }) => { const workbenchMode = true; const { t } = useTranslation("pickOrder"); + const L = useUiText(); const router = useRouter(); const pathname = usePathname(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -743,10 +745,10 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); .toUpperCase() .replace(/\s/g, ""); const floorKey = storeId.replace(/\//g, ""); - if (floorKey === FLOOR_A_KEY) return LABEL_A; - if (floorKey === FLOOR_B_KEY) return LABEL_B; + if (floorKey === FLOOR_A_KEY) return L("Label機 2F A+B", "Label printer 2F A+B"); + if (floorKey === FLOOR_B_KEY) return L("Label機 4F 乾貨 C, D", "Label printer 4F dry goods C, D"); return undefined; - }, [fgPickOrders]); + }, [fgPickOrders, L]); const [workbenchLotLabelModalOpen, setWorkbenchLotLabelModalOpen] = useState(false); diff --git a/src/components/EquipmentSearch/EquipmentSearch.tsx b/src/components/EquipmentSearch/EquipmentSearch.tsx index dbe7db73..6933cecd 100644 --- a/src/components/EquipmentSearch/EquipmentSearch.tsx +++ b/src/components/EquipmentSearch/EquipmentSearch.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { EquipmentResult } from "@/app/api/settings/equipment"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import EquipmentSearchResults, { Column } from "./EquipmentSearchResults"; import { EditNote } from "@mui/icons-material"; import { useRouter } from "next/navigation"; @@ -42,7 +43,9 @@ type SearchParamNames = keyof SearchQuery; const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { const [filteredEquipments, setFilteredEquipments] = useState([]); - const { t } = useTranslation(["equipment", "common"]); + const { t, i18n } = useTranslation(["equipment", "common"]); + const L = useUiText(); + const lang = i18n.resolvedLanguage || i18n.language; const router = useRouter(); const [filterObjByTab, setFilterObjByTab] = useState>({ 0: {}, @@ -119,23 +122,28 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { if (tabIndex === 1) { return [ { - label: "設備名稱/設備編號", + label: L("設備名稱/設備編號", "Equipment name / code"), paramName: "equipmentCode", type: "text" }, { label: t("Repair and Maintenance Status"), paramName: "repairAndMaintenanceStatus", - type: "select", - options: ["正常使用中", "正在維護中"] + type: "select-labelled", + options: [ + { value: "正常使用中", label: L("正常使用中", "In use") }, + { value: "正在維護中", label: L("正在維護中", "Under maintenance") }, + ], }, ]; } return [ - { label: "設備編號", paramName: "code", type: "text" }, + { label: L("設備編號", "Equipment code"), paramName: "code", type: "text" }, ]; - }, [t, tabIndex]); + // lang tracks the UI language. L is a new function on every render. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [t, tabIndex, lang]); const onMaintenanceEditClick = useCallback( (equipment: EquipmentResult) => { @@ -193,13 +201,14 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { } } catch (error) { console.error("Error deleting equipment detail:", error); - alert("刪除失敗,請稍後再試"); + alert(L("刪除失敗,請稍後再試", "Could not delete. Try again later.")); } finally { setDeleting(false); setDeleteDialogOpen(false); setItemToDelete(null); } - }, [itemToDelete]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [itemToDelete, lang]); const handleDeleteCancel = useCallback(() => { setDeleteDialogOpen(false); @@ -412,7 +421,7 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { () => [ { name: "code", - label: "設備編號", + label: L("設備編號", "Equipment code"), renderCell: (item) => ( = ({ equipments, tabIndex = 0 }) => { ), }, ], - [t, handleToggleExpand, expandedRows], + // eslint-disable-next-line react-hooks/exhaustive-deps + [t, handleToggleExpand, expandedRows, lang], ); const repairMaintenanceColumns = useMemo[]>( () => [ { name: "id", - label: "編輯", + label: L("編輯", "Edit"), onClick: onMaintenanceEditClick, buttonIcon: , align: "left", @@ -450,14 +460,14 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { }, { name: "code", - label: "設備名稱", + label: L("設備名稱", "Equipment name"), align: "left", headerAlign: "left", sx: { width: "200px", minWidth: "200px" }, }, { name: "equipmentCode", - label: "設備編號", + label: L("設備編號", "Equipment code"), align: "left", headerAlign: "left", sx: { width: "150px", minWidth: "150px" }, @@ -476,13 +486,13 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { if (status === 1 || status === true) { return ( - 正在維護中 + {L("正在維護中", "Under maintenance")} ); } else if (status === 0 || status === false) { return ( - 正常使用中 + {L("正常使用中", "In use")} ); } @@ -513,7 +523,8 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { sx: { width: "200px", minWidth: "200px" }, }, ], - [onMaintenanceEditClick, t, displayDateTime], + // eslint-disable-next-line react-hooks/exhaustive-deps + [onMaintenanceEditClick, t, displayDateTime, lang], ); const columns = useMemo(() => { @@ -617,11 +628,11 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { if (!isExistingCombination) { if (equipmentCodePrefix.length !== 3 || !/^[A-Z]{3}$/.test(equipmentCodePrefix)) { - alert("請輸入3個大寫英文字母作為設備編號前綴"); + alert(L("請輸入3個大寫英文字母作為設備編號前綴", "Enter 3 uppercase letters as the equipment code prefix.")); return; } if (!equipmentCodeNumber) { - alert("設備編號生成中,請稍候"); + alert(L("設備編號生成中,請稍候", "The equipment code is still being generated. Please wait.")); return; } } @@ -737,15 +748,16 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { } } - alert("新增成功"); + alert(L("新增成功", "Added.")); } catch (error: any) { console.error("Error saving equipment detail:", error); - const errorMessage = error.response?.data?.message || error.message || "保存失敗,請稍後再試"; + const errorMessage = error.response?.data?.message || error.message || L("保存失敗,請稍後再試", "Could not save. Try again later."); alert(errorMessage); } finally { setSaving(false); } - }, [selectedName, selectedDescription, selectedEquipmentCode, equipmentCodePrefix, equipmentCodeNumber, isExistingCombination, equipmentList, refetchData, filterObj, handleAddDialogClose, tabIndex, equipmentDetailsMap, fetchEquipmentDetailsByEquipmentId]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedName, selectedDescription, selectedEquipmentCode, equipmentCodePrefix, equipmentCodeNumber, isExistingCombination, equipmentList, refetchData, filterObj, handleAddDialogClose, tabIndex, equipmentDetailsMap, fetchEquipmentDetailsByEquipmentId, lang]); const renderExpandedRow = useCallback((item: EquipmentResult): React.ReactNode => { if (tabIndex !== 0) { @@ -763,14 +775,14 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { {isLoading ? ( - 載入中... + {L("載入中...", "Loading...")} ) : details.length === 0 ? ( - 無相關設備詳細資料 + {L("無相關設備詳細資料", "No equipment details")} ) : ( - 設備詳細資料 (設備編號: {item.code}) + {L(`設備詳細資料 (設備編號: ${item.code})`, `Equipment details (code: ${item.code})`)} {details.map((detail) => ( @@ -787,7 +799,7 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { > - 編號: {detail.code || "-"} + {L("編號:", "Code:")} {detail.code || "-"} = ({ equipments, tabIndex = 0 }) => { {detail.name && ( - 名稱: {detail.name} + {L("名稱:", "Name:")} {detail.name} )} {detail.description && ( - 描述: {detail.description} + {L("描述:", "Description:")} {detail.description} )} {detail.equipmentCode && ( - 設備編號: {detail.equipmentCode} + {L("設備編號:", "Equipment code:")} {detail.equipmentCode} )} @@ -824,7 +836,8 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { ); - }, [columns.length, equipmentDetailsMap, loadingDetailsMap, expandedRows, tabIndex, handleDeleteClick]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [columns.length, equipmentDetailsMap, loadingDetailsMap, expandedRows, tabIndex, handleDeleteClick, lang]); return ( <> @@ -842,7 +855,7 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { {tabIndex === 0 && ( - 設備編號 + {L("設備編號", "Equipment code")} )} @@ -891,19 +904,19 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { aria-describedby="delete-dialog-description" > - 確認刪除 + {L("確認刪除", "Confirm delete")} - 您確定要刪除此設備詳細資料嗎?此操作無法復原。 + {L("您確定要刪除此設備詳細資料嗎?此操作無法復原。", "Delete this equipment detail? This cannot be undone.")} @@ -918,7 +931,7 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { fullWidth > - 新增設備詳細資料 + {L("新增設備詳細資料", "Add equipment detail")} @@ -937,8 +950,8 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { renderInput={(params) => ( )} sx={{ mb: 2 }} @@ -973,15 +986,15 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { renderInput={(params) => ( )} /> { if (!isExistingCombination) { @@ -990,7 +1003,7 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { } }} disabled={isExistingCombination || loadingEquipments || saving} - placeholder={isExistingCombination ? "自動生成" : "輸入3個大寫英文字母"} + placeholder={isExistingCombination ? L("自動生成", "Generated automatically") : L("輸入3個大寫英文字母", "Enter 3 uppercase letters")} required={!isExistingCombination} InputProps={{ endAdornment: !isExistingCombination && equipmentCodeNumber ? ( @@ -1010,9 +1023,9 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { ) : null, }} helperText={!isExistingCombination && equipmentCodePrefix.length > 0 && equipmentCodePrefix.length !== 3 - ? "必須輸入3個大寫英文字母" + ? L("必須輸入3個大寫英文字母", "Enter 3 uppercase letters") : !isExistingCombination && equipmentCodePrefix.length === 3 && !/^[A-Z]{3}$/.test(equipmentCodePrefix) - ? "必須是大寫英文字母" + ? L("必須是大寫英文字母", "Use uppercase letters only") : ""} error={!isExistingCombination && equipmentCodePrefix.length > 0 && (equipmentCodePrefix.length !== 3 || !/^[A-Z]{3}$/.test(equipmentCodePrefix))} /> @@ -1021,14 +1034,14 @@ const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { diff --git a/src/components/FinishedGoodSearch/EscalationComponent.tsx b/src/components/FinishedGoodSearch/EscalationComponent.tsx index 12e0b81e..ccc8be37 100644 --- a/src/components/FinishedGoodSearch/EscalationComponent.tsx +++ b/src/components/FinishedGoodSearch/EscalationComponent.tsx @@ -1,3 +1,4 @@ +import { useUiText } from "@/i18n/useUiText"; import React, { useState, ChangeEvent, FormEvent, Dispatch } from 'react'; import { Box, @@ -44,6 +45,7 @@ const EscalationComponent: React.FC = ({ setIsCollapsed }) => { const { t } = useTranslation("purchaseOrder"); + const L = useUiText(); const [formData, setFormData] = useState({ name: '', @@ -52,12 +54,12 @@ const EscalationComponent: React.FC = ({ }); const nameOptions: NameOption[] = [ - { value: '', label: '請選擇姓名...' }, - { value: 'john', label: '張大明' }, - { value: 'jane', label: '李小美' }, - { value: 'mike', label: '王志強' }, - { value: 'sarah', label: '陳淑華' }, - { value: 'david', label: '林建國' }, + { value: '', label: L('請選擇姓名...', 'Select a name...') }, + { value: 'john', label: L('張大明', 'John') }, + { value: 'jane', label: L('李小美', 'Jane') }, + { value: 'mike', label: L('王志強', 'Mike') }, + { value: 'sarah', label: L('陳淑華', 'Sarah') }, + { value: 'david', label: L('林建國', 'David') }, ]; const handleInputChange = ( @@ -96,7 +98,7 @@ const EscalationComponent: React.FC = ({ } label={ - 上報結果 + {L("上報結果", "Escalation result")} {isCollapsed ? ( ) : ( @@ -116,8 +118,8 @@ const EscalationComponent: React.FC = ({ defaultValue="pass" name="radio-buttons-group" > - } label="合格" /> - } label="不合格" /> + } label={L("合格", "Pass")} /> + } label={L("不合格", "Fail")} /> ): undefined} @@ -140,24 +142,24 @@ const EscalationComponent: React.FC = ({ fullWidth id="quantity" name="quantity" - label="數量" + label={L("數量", "Quantity")} type="number" value={formData.quantity} onChange={handleInputChange} InputProps={{ inputProps: { min: 1 } }} - placeholder="請輸入數量" + placeholder={L("請輸入數量", "Enter quantity")} /> diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx index b1f0f645..e9ba91f4 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx @@ -1,4 +1,5 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; import { PickOrderResult } from "@/app/api/pickOrder"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -66,6 +67,7 @@ type SearchParamNames = keyof SearchQuery; const PickOrderSearch: React.FC = ({ printerCombo }) => { const { t } = useTranslation("pickOrder"); + const L = useUiText(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; @@ -301,7 +303,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState {isTruckRoutingTab ? isPrintingRoutingSummary - ? "生成中..." - : "列印報告" + ? L("生成中...", "Generating...") + : L("列印報告", "Print report") : `${t("Print All Draft")} (${releasedOrderCount})`} {/* @@ -834,8 +836,8 @@ const handleAssignByLane = useCallback(async ( - - + + diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index 8ad3c779..805ef23b 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -1,7 +1,8 @@ /* "use client"; -import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors"; +import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors"; +import { useUiText } from "@/i18n/useUiText"; import { Box, Button, @@ -686,6 +687,7 @@ const PickExecution: React.FC = ({ onRefreshReleasedOrderCount, }) => { const { t } = useTranslation("pickOrder"); + const L = useUiText(); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const [doPickOrderDetail, setDoPickOrderDetail] = @@ -797,10 +799,10 @@ const PickExecution: React.FC = ({ .toUpperCase() .replace(/\s/g, ""); const floorKey = storeId.replace(/\//g, ""); - if (floorKey === FLOOR_A_KEY) return LABEL_A; - if (floorKey === FLOOR_B_KEY) return LABEL_B; + if (floorKey === FLOOR_A_KEY) return L("Label機 2F A+B", "Label printer 2F A+B"); + if (floorKey === FLOOR_B_KEY) return L("Label機 4F 乾貨 C, D", "Label printer 4F dry goods C, D"); return undefined; - }, [fgPickOrders]); + }, [fgPickOrders, L]); // Add these missing state variables after line 352 const [isManualScanning, setIsManualScanning] = useState(false); // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling @@ -1714,7 +1716,7 @@ const PickExecution: React.FC = ({ if (Number.isFinite(itemId) && Number.isFinite(stockInLineId)) { setLotLabelPrintInitialPayload({ itemId, stockInLineId }); setLotLabelPrintReminderText( - "該批次不可用,請移除該Label並列印新Label。", + L("該批次不可用,請移除該Label並列印新Label。", "This lot is unavailable. Remove that label and print a new one."), ); setLotLabelPrintModalOpen(true); } @@ -2347,9 +2349,10 @@ const PickExecution: React.FC = ({ setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( - `此批次(${ - scannedLot.lotNo || scannedStockInLineId - })已被拒绝,无法使用。请扫描其他批次。`, + L( + `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`, + `This lot (${scannedLot.lotNo || scannedStockInLineId}) was rejected and cannot be used. Scan another lot.`, + ), ); }); // Mark as processed to prevent re-processing @@ -2374,9 +2377,10 @@ const PickExecution: React.FC = ({ setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( - `此批次(${ - scannedLot.lotNo || scannedStockInLineId - })已过期,无法使用。请扫描其他批次。`, + L( + `此批次(${scannedLot.lotNo || scannedStockInLineId})已过期,无法使用。请扫描其他批次。`, + `This lot (${scannedLot.lotNo || scannedStockInLineId}) is expired and cannot be used. Scan another lot.`, + ), ); }); // Mark as processed to prevent re-processing the same expired QR repeatedly @@ -2399,7 +2403,7 @@ const PickExecution: React.FC = ({ startTransition(() => { setQrScanError(true); setQrScanSuccess(false); - setQrScanErrorMsg("当前订单中没有此物品的批次信息"); + setQrScanErrorMsg(L("当前订单中没有此物品的批次信息", "This order has no lot information for this item")); }); return; } @@ -5075,7 +5079,7 @@ const PickExecution: React.FC = ({ defaultPrinterName={defaultLabelPrinterName} hideScanSection reminderText={lotLabelPrintReminderText ?? undefined} - statusTitleText="此批號的已用完/已過期" + statusTitleText={L("此批號的已用完/已過期", "This lot is used up or expired")} warehouseCodePrefixFilter={lotFloorPrefixFilter} hideTriggeredLot /> diff --git a/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx b/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx index 9608cb61..ca5f108a 100644 --- a/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx +++ b/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx @@ -1,5 +1,6 @@ "use client"; // 修改为 PickOrder 相关的导入 +import { useUiText } from "@/i18n/useUiText"; import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; import { QcItemWithChecks } from "@/app/api/qc"; import { PurchaseQcResult } from "@/app/api/po/actions"; @@ -79,6 +80,7 @@ const PickQcStockInModalVer2: React.FC = ({ t, i18n: { language }, } = useTranslation("pickOrder"); + const L = useUiText(); const [qcItems, setQcItems] = useState(dummyQCData) const formProps = useForm({ @@ -126,7 +128,7 @@ const PickQcStockInModalVer2: React.FC = ({ if (validationErrors.length > 0) { console.error("QC Validation failed:", validationErrors); - alert(`未完成品檢: ${validationErrors}`); + alert(L(`未完成品檢: ${validationErrors}`, `QC incomplete: ${validationErrors}`)); return; } @@ -150,7 +152,7 @@ const PickQcStockInModalVer2: React.FC = ({ submitDialogWithWarning(() => { console.log("QC accepted with failed items"); onClose?.({} as object, "backdropClick"); - }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); + }, t, {title: L("有不合格檢查項目,確認接受收貨?", "Some checks failed. Accept this receipt anyway?"), confirmButtonText: "Confirm", html: ""}); return; } @@ -191,7 +193,7 @@ const PickQcStockInModalVer2: React.FC = ({ GroupA - {itemDetail.pickOrderCode} - 記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標 + {L("記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標", "Record the time the temperature was probed. Finish the issue within 1 hour to keep food safe. Check by sight, smell, and a suitable food thermometer that the food temperature meets the standard.")} @@ -201,8 +203,8 @@ const PickQcStockInModalVer2: React.FC = ({
- QC模板代號 - 檢查項目 + {L("QC模板代號", "QC template code")} + {L("檢查項目", "Check item")} QC Result Failed Qty Remarks diff --git a/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx b/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx index 8b9f7232..8956217e 100644 --- a/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx +++ b/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { GetPickOrderLineInfo, updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; import { QcItemWithChecks } from "@/app/api/qc"; import { PurchaseQcResult } from "@/app/api/po/actions"; @@ -144,6 +145,7 @@ const PickQcStockInModalVer3: React.FC = ({ t, i18n: { language }, } = useTranslation("pickOrder"); + const L = useUiText(); const [tabIndex, setTabIndex] = useState(0); //const [qcItems, setQcItems] = useState(dummyQCData); @@ -403,7 +405,7 @@ const PickQcStockInModalVer3: React.FC = ({ if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) { submitDialogWithWarning(() => { closeHandler?.({}, 'escapeKeyDown'); - }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); + }, t, {title: L("有不合格檢查項目,確認接受出庫?", "Some checks failed. Accept this issue anyway?"), confirmButtonText: "Confirm", html: ""}); return; } @@ -462,7 +464,7 @@ const PickQcStockInModalVer3: React.FC = ({ } - label="合格" + label={L("合格", "Pass")} sx={{ color: current.qcPassed === true ? "green" : "inherit", "& .Mui-checked": {color: "green"} @@ -471,7 +473,7 @@ const PickQcStockInModalVer3: React.FC = ({ } - label="不合格" + label={L("不合格", "Fail")} sx={{ color: current.qcPassed === false ? "red" : "inherit", "& .Mui-checked": {color: "red"} @@ -578,15 +580,15 @@ const PickQcStockInModalVer3: React.FC = ({ - Group A - 急凍貨類 (QCA1-MEAT01) + {L("Group A - 急凍貨類 (QCA1-MEAT01)", "Group A - Frozen goods (QCA1-MEAT01)")} - 品檢類型:OQC + {L("品檢類型", "QC type")}:OQC - 記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全
- 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標 + {L("記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全", "Record the probe time and finish the outbound stock check within 1 hour to keep food safe.")}
+ {L("監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標", "Method: visual check, smell check, and a suitable food thermometer to confirm the food temperature meets the standard.")}
diff --git a/src/components/FinishedGoodSearch/QCDatagrid.tsx b/src/components/FinishedGoodSearch/QCDatagrid.tsx index b9947dbb..afb443f6 100644 --- a/src/components/FinishedGoodSearch/QCDatagrid.tsx +++ b/src/components/FinishedGoodSearch/QCDatagrid.tsx @@ -1,4 +1,5 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { Dispatch, MutableRefObject, @@ -113,6 +114,7 @@ function InputDataGrid({ t, // i18n: { language }, } = useTranslation("purchaseOrder"); + const L = useUiText(); const formKey = _formKey.toString(); const { setValue, getValues } = useFormContext(); const [rowModesModel, setRowModesModel] = useState({}); @@ -310,7 +312,7 @@ function InputDataGrid({ onClick={addRow} size="small" > - 新增 + {L("新增", "Add")} {/* {t("Add Record")} */} ); diff --git a/src/components/FinishedGoodSearch/QcFormVer2.tsx b/src/components/FinishedGoodSearch/QcFormVer2.tsx index ebea29d1..b1dc731f 100644 --- a/src/components/FinishedGoodSearch/QcFormVer2.tsx +++ b/src/components/FinishedGoodSearch/QcFormVer2.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions"; import { Box, @@ -70,6 +71,7 @@ type QcRow = TableRow, EntryError>; // fetchQcItemCheck const QcFormVer2: React.FC = ({ qc, itemDetail, disabled, qcItems, setQcItems }) => { const { t } = useTranslation("purchaseOrder"); + const L = useUiText(); const apiRef = useGridApiRef(); const { register, @@ -216,7 +218,7 @@ const QcFormVer2: React.FC = ({ qc, itemDetail, disabled, qcItems, setQcI } - label="合格" + label={L("合格", "Pass")} sx={{ color: currentValue === true ? "green" : "inherit", "& .Mui-checked": {color: "green"} @@ -225,7 +227,7 @@ const QcFormVer2: React.FC = ({ qc, itemDetail, disabled, qcItems, setQcI } - label="不合格" + label={L("不合格", "Fail")} sx={{ color: currentValue === false ? "red" : "inherit", "& .Mui-checked": {color: "red"} @@ -419,7 +421,7 @@ const QcFormVer2: React.FC = ({ qc, itemDetail, disabled, qcItems, setQcI field.onChange(value); }} > - } label="接受" /> + } label={L("接受", "Accept")} /> = ({ qc, itemDetail, disabled, qcItems, setQcI helperText={errors.acceptQty?.message} /> - } label="不接受及上報" /> + } label={L("不接受及上報", "Reject and escalate")} /> )} /> diff --git a/src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx b/src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx index b6f7a412..53ee7077 100644 --- a/src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx +++ b/src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; import { useEffect, useState } from "react"; import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material"; @@ -29,6 +30,7 @@ interface Props { } const TruckRoutingSummaryTab: React.FC = ({ onFiltersChange }) => { + const L = useUiText(); const [storeOptions, setStoreOptions] = useState([]); const [laneOptions, setLaneOptions] = useState([]); const [storeId, setStoreId] = useState(""); @@ -70,7 +72,10 @@ const TruckRoutingSummaryTab: React.FC = ({ onFiltersChange }) => { }); if (precheck.hasUnpickedOrders) { const confirmed = window.confirm( - `此車線仍有 ${precheck.unpickedOrderCount} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?` + L( + `此車線仍有 ${precheck.unpickedOrderCount} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?`, + `This truck lane still has ${precheck.unpickedOrderCount} unpicked orders.\nPrint or download the delivery route summary anyway?`, + ) ); if (!confirmed) return; } @@ -107,7 +112,7 @@ const TruckRoutingSummaryTab: React.FC = ({ onFiltersChange }) => { ); } catch (error) { console.error("Failed to download Truck Routing Summary", error); - alert("下載送貨路線摘要失敗,請稍後再試。"); + alert(L("下載送貨路線摘要失敗,請稍後再試。", "Could not download the delivery route summary. Try again later.")); } finally { setLoading(false); } @@ -116,13 +121,13 @@ const TruckRoutingSummaryTab: React.FC = ({ onFiltersChange }) => { return ( - 送貨路線摘要 + {L("送貨路線摘要", "Delivery route summary")} onStoreChange(e.target.value)} > @@ -135,7 +140,7 @@ const TruckRoutingSummaryTab: React.FC = ({ onFiltersChange }) => { setTruckLanceCode(e.target.value)} disabled={!storeId} @@ -148,7 +153,7 @@ const TruckRoutingSummaryTab: React.FC = ({ onFiltersChange }) => { = ({ onFiltersChange }) => { disabled={!canDownload} onClick={onDownload} > - {loading ? "生成中..." : "下載報告 (PDF)"} + {loading ? L("生成中...", "Generating...") : L("下載報告 (PDF)", "Download report (PDF)")} ); diff --git a/src/components/FinishedGoodSearch/pickorderModelVer2.tsx b/src/components/FinishedGoodSearch/pickorderModelVer2.tsx index 2bd870fe..b7607a94 100644 --- a/src/components/FinishedGoodSearch/pickorderModelVer2.tsx +++ b/src/components/FinishedGoodSearch/pickorderModelVer2.tsx @@ -1,5 +1,6 @@ "use client"; // 修改为 PickOrder 相关的导入 +import { useUiText } from "@/i18n/useUiText"; import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; import { QcItemWithChecks } from "@/app/api/qc"; import { PurchaseQcResult } from "@/app/api/po/actions"; @@ -77,6 +78,7 @@ const PickQcStockInModalVer2: React.FC = ({ t, i18n: { language }, } = useTranslation("pickOrder"); + const L = useUiText(); const [qcItems, setQcItems] = useState(dummyQCData) const formProps = useForm({ defaultValues: { @@ -159,7 +161,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) if (validationErrors.length > 0) { console.error("QC Validation failed:", validationErrors); - alert(`未完成品檢: ${validationErrors}`); + alert(L(`未完成品檢: ${validationErrors}`, `QC incomplete: ${validationErrors}`)); return; } @@ -181,7 +183,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) // await submitQcData(qcData); if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { - submitDialogWithWarning(onOpenPutaway, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); + submitDialogWithWarning(onOpenPutaway, t, {title: L("有不合格檢查項目,確認接受收貨?", "Some checks failed. Accept this receipt anyway?"), confirmButtonText: "Confirm", html: ""}); return; } diff --git a/src/components/ImportBom/ImportBomDetailTab.tsx b/src/components/ImportBom/ImportBomDetailTab.tsx index 3467f120..1781e480 100644 --- a/src/components/ImportBom/ImportBomDetailTab.tsx +++ b/src/components/ImportBom/ImportBomDetailTab.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import React, { useEffect, useRef, useState } from "react"; import { Box, @@ -65,6 +66,7 @@ const SHOW_BOM_FULL_EDIT = false; const ImportBomDetailTab: React.FC = () => { const { t } = useTranslation(["importBom", "common"]); + const L = useUiText(); const [bomList, setBomList] = useState([]); const [selectedBomId, setSelectedBomId] = useState(""); const [detail, setDetail] = useState(null); @@ -251,34 +253,34 @@ const ImportBomDetailTab: React.FC = () => { [bomList, loadBomDetail], ); const renderAllergic = (v?: number) => { - if (v === 0) return "有"; - if (v === 5) return "沒有"; + if (v === 0) return L("有", "Yes"); + if (v === 5) return L("沒有", "No"); return "-"; }; const renderIsFloat = (v?: number) => { - if (v === 5) return "沉"; - if (v === 3) return "浮"; - if (v === 0) return "不適用"; + if (v === 5) return L("沉", "Sinks"); + if (v === 3) return L("浮", "Floats"); + if (v === 0) return L("不適用", "N/A"); return "-"; }; const renderIsDense = (v?: number) => { - if (v === 5) return "淡"; - if (v === 3) return "濃"; - if (v === 0) return "不適用"; + if (v === 5) return L("淡", "Mild"); + if (v === 3) return L("濃", "Strong"); + if (v === 0) return L("不適用", "N/A"); return "-"; }; const renderTimeSequence = (v?: number) => { - if (v === 5) return "上午"; - if (v === 1) return "下午"; - if (v === 0) return "不適用"; + if (v === 5) return L("上午", "Morning"); + if (v === 1) return L("下午", "Afternoon"); + if (v === 0) return L("不適用", "N/A"); return "-"; }; const renderType = (v?: string) => { - if (v === "FG") return "成品"; - if (v === "WIP") return "半成品"; + if (v === "FG") return L("成品", "Finished goods"); + if (v === "WIP") return L("半成品", "Semi-finished"); return "-"; }; @@ -315,10 +317,10 @@ const ImportBomDetailTab: React.FC = () => { }, [detail?.id, statusDraft]); const renderComplexity = (v?: number) => { - if (v === 10) return "簡單"; - if (v === 5) return "中度"; - if (v === 3) return "複雜"; - if (v === 0) return "不適用"; + if (v === 10) return L("簡單", "Simple"); + if (v === 5) return L("中度", "Moderate"); + if (v === 3) return L("複雜", "Complex"); + if (v === 0) return L("不適用", "N/A"); return "-"; }; /* @@ -402,7 +404,7 @@ const ImportBomDetailTab: React.FC = () => { const msg = e && typeof e === "object" && "message" in e ? String((e as { message?: string }).message) - : "載入製程/設備主檔失敗"; + : L("載入製程/設備主檔失敗", "Could not load process and equipment master data"); setEditError(msg); } finally { setEditMasterLoading(false); @@ -468,21 +470,21 @@ const ImportBomDetailTab: React.FC = () => { const addProcessFromForm = useCallback(() => { const pCode = processAddForm.processCode.trim(); if (!pCode) { - setEditError("請先選擇工序 Process Code"); + setEditError(L("請先選擇工序 Process Code", "Select a process code first")); return; } const ed = processAddForm.equipmentDescription.trim(); const en = processAddForm.equipmentName.trim(); if ((ed && !en) || (!ed && en)) { - setEditError("設備描述與名稱需同時選取,或同時留空(不適用)"); + setEditError(L("設備描述與名稱需同時選取,或同時留空(不適用)", "Select both the equipment description and name, or leave both blank (N/A)")); return; } if (ed && en) { const resolved = resolveEquipmentCode(equipmentMasterList, ed, en); if (!resolved) { setEditError( - `設備組合「${ed}-${en}」在主檔中找不到對應設備代碼,請確認後再試`, + L(`設備組合「${ed}-${en}」在主檔中找不到對應設備代碼,請確認後再試`, `Equipment pair "${ed}-${en}" has no equipment code in master data. Check it and try again`), ); return; } @@ -532,18 +534,18 @@ const ImportBomDetailTab: React.FC = () => { try { for (const p of editProcesses) { if (!p.processCode?.trim()) { - throw new Error("工序行 Process Code 不能为空"); + throw new Error(L("工序行 Process Code 不能为空", "Process line process code is required")); } const ed = p.equipmentDescription.trim(); const en = p.equipmentName.trim(); if ((ed && !en) || (!ed && en)) { - throw new Error("各製程行的設備描述與名稱需同時填寫或同時留空"); + throw new Error(L("各製程行的設備描述與名稱需同時填寫或同時留空", "Each process line needs both an equipment description and name, or both left blank")); } if (ed && en) { const resolved = resolveEquipmentCode(equipmentMasterList, ed, en); if (!resolved) { throw new Error( - `設備「${ed}-${en}」在主檔中無對應設備代碼,請修正後再儲存`, + L(`設備「${ed}-${en}」在主檔中無對應設備代碼,請修正後再儲存`, `Equipment "${ed}-${en}" has no equipment code in master data. Fix it before saving`), ); } } @@ -587,7 +589,7 @@ const ImportBomDetailTab: React.FC = () => { setDetail(updated); setIsEditing(false); } catch (e: any) { - setEditError(e?.message || "保存失败"); + setEditError(e?.message || L("保存失败", "Save failed")); } finally { setEditLoading(false); } @@ -605,7 +607,7 @@ const ImportBomDetailTab: React.FC = () => { {filteredBoms.length > 1 && ( - 找到多筆 BOM,請選擇一筆載入明細 + {L("找到多筆 BOM,請選擇一筆載入明細", "Several BOMs were found. Select one to load its details")} {filteredBoms.map((b) => ( @@ -813,8 +815,8 @@ const ImportBomDetailTab: React.FC = () => { ) } > - 有 - 沒有 + {L("有", "Yes")} + {L("沒有", "No")} @@ -843,9 +845,9 @@ const ImportBomDetailTab: React.FC = () => { ) } > - 沉 - 浮 - 不適用 + {L("沉", "Sinks")} + {L("浮", "Floats")} + {L("不適用", "N/A")} @@ -859,9 +861,9 @@ const ImportBomDetailTab: React.FC = () => { ) } > - 淡 - 濃 - 不適用 + {L("淡", "Mild")} + {L("濃", "Strong")} + {L("不適用", "N/A")} @@ -880,9 +882,9 @@ const ImportBomDetailTab: React.FC = () => { ) } > - 上午 - 下午 - 不適用 + {L("上午", "Morning")} + {L("下午", "Afternoon")} + {L("不適用", "N/A")} @@ -898,10 +900,10 @@ const ImportBomDetailTab: React.FC = () => { ) } > - 簡單 - 中度 - 複雜 - 不適用 + {L("簡單", "Simple")} + {L("中度", "Moderate")} + {L("複雜", "Complex")} + {L("不適用", "N/A")} @@ -1003,7 +1005,7 @@ const ImportBomDetailTab: React.FC = () => { } > - 請選擇 + {L("請選擇", "Select")} {processCodeOptions.map((c) => ( @@ -1014,9 +1016,9 @@ const ImportBomDetailTab: React.FC = () => { - 設備說明 + {L("設備說明", "Equipment description")} setProcessAddForm((p) => ({ @@ -1046,7 +1048,7 @@ const ImportBomDetailTab: React.FC = () => { })) } > - 不適用 + {L("不適用", "N/A")} {equipmentNameOptions.map((c) => ( {c} @@ -1122,7 +1124,7 @@ const ImportBomDetailTab: React.FC = () => { {t("Process Name")} {t("Process Description")} {/* {t("Process Code")}*/} - 設備(說明/名稱) + {L("設備(說明/名稱)", "Equipment (description / name)")} {t("Duration (Minutes)")} {t("Prep Time (Minutes)")} {t("Post Prod Time (Minutes)")} @@ -1169,7 +1171,7 @@ const ImportBomDetailTab: React.FC = () => { ) } > - 不適用 + {L("不適用", "N/A")} {processCodeOptions.map((c) => ( {c} @@ -1199,7 +1201,7 @@ const ImportBomDetailTab: React.FC = () => { ) } > - 不適用 + {L("不適用", "N/A")} {equipmentDescriptionOptions.map((c) => ( {c} @@ -1224,7 +1226,7 @@ const ImportBomDetailTab: React.FC = () => { ) } > - 不適用 + {L("不適用", "N/A")} {equipmentNameOptions.map((c) => ( {c} diff --git a/src/components/ImportBom/ImportBomResultForm.tsx b/src/components/ImportBom/ImportBomResultForm.tsx index 20fcda47..9d6e584b 100644 --- a/src/components/ImportBom/ImportBomResultForm.tsx +++ b/src/components/ImportBom/ImportBomResultForm.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import React, { useMemo, useState } from "react"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; @@ -43,6 +44,7 @@ type Props = { }: Props) { console.log("issueLogFileId from props", issueLogFileId); const { t } = useTranslation(["importBom", "common", "importExcel"]); + const L = useUiText(); const [search, setSearch] = useState(""); const [items, setItems] = useState(() => correctFileNames.map((fileName) => ({ @@ -108,10 +110,10 @@ type Props = { //a.download = `bom_excel_issue_log_${new Date().toISOString().slice(0, 10)}.xlsx`; //a.click(); //URL.revokeObjectURL(url); - setSuccessMsg("匯入完成"); + setSuccessMsg(L("匯入完成", "Import complete")); } catch (err) { console.error(err); - setSuccessMsg("匯入失敗,請查看主控台。"); + setSuccessMsg(L("匯入失敗,請查看主控台。", "Import failed. Check the console.")); } finally { setSubmitting(false); } @@ -127,16 +129,16 @@ type Props = { {onBack && ( )} - 已上傳 {uploadedCount} 個檔案,檢查結果共 {totalChecked} 筆:正確 {correctFileNames.length} 個、失敗 {failList.length} 個 + {L(`已上傳 ${uploadedCount} 個檔案,檢查結果共 ${totalChecked} 筆:正確 ${correctFileNames.length} 個、失敗 ${failList.length} 個`, `Uploaded ${uploadedCount} files. Check results: ${totalChecked} rows, ${correctFileNames.length} correct, ${failList.length} failed`)} {uploadedCount !== totalChecked && ( - 上傳數與檢查筆數不符,可能因檔名重複;重新上傳後會為重複檔名自動加 _2、_3 等區分,全部都會列入檢查。 + {L("上傳數與檢查筆數不符,可能因檔名重複;重新上傳後會為重複檔名自動加 _2、_3 等區分,全部都會列入檢查。", "The upload count does not match the check count, possibly because of duplicate file names. After you upload again, duplicates are renamed with _2, _3, and so on, and every file is included in the check.")} )} @@ -156,7 +158,7 @@ type Props = { setSearch(e.target.value)} InputProps={{ @@ -216,7 +218,7 @@ type Props = { {failList.length === 0 ? ( - 無 + {L("無", "None")} ) : ( failList.map((f) => ( @@ -252,14 +254,14 @@ type Props = { onClick={handleConfirm} disabled={submitting || items.length === 0} > - 確認匯入 + {L("確認匯入", "Confirm import")} {submitting && } {successMsg && ( @@ -270,10 +272,10 @@ type Props = { {items.length > 0 && ( - 將匯入 {items.length} 個 BOM - {wipCount > 0 ? `,其中 ${wipCount} 個同時建立 半成品` : ""} - {drinkCount > 0 ? `,${drinkCount} 個飲料` : ""} - {powderMixtureCount > 0 ? `,${powderMixtureCount} 個箱料粉` : ""} + {L(`將匯入 ${items.length} 個 BOM`, `${items.length} BOMs will be imported`)} + {wipCount > 0 ? L(`,其中 ${wipCount} 個同時建立 半成品`, `, including ${wipCount} that also create a semi-finished item`) : ""} + {drinkCount > 0 ? L(`,${drinkCount} 個飲料`, `, ${drinkCount} drinks`) : ""} + {powderMixtureCount > 0 ? L(`,${powderMixtureCount} 個箱料粉`, `, ${powderMixtureCount} powder mixes`) : ""} )} diff --git a/src/components/ImportBom/ImportBomUpload.tsx b/src/components/ImportBom/ImportBomUpload.tsx index ab48a9ae..6b55a360 100644 --- a/src/components/ImportBom/ImportBomUpload.tsx +++ b/src/components/ImportBom/ImportBomUpload.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import React, { useState } from "react"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; @@ -22,21 +23,22 @@ type Props = { ) => void; }; -function getErrorMessage(err: unknown): string { +function getErrorMessage(err: unknown, L: (zh: string, en: string) => string): string { if (err && typeof err === "object" && "response" in err) { const res = (err as { response?: { status?: number; data?: unknown } }) .response; if (res?.status === 500) - return "伺服器錯誤 (500),請確認後端服務已啟動且 API 路徑正確。"; + return L("伺服器錯誤 (500),請確認後端服務已啟動且 API 路徑正確。", "Server error (500). Confirm the backend is running and the API path is correct."); if (res?.data && typeof res.data === "string" && res.data.length < 200) return res.data; } if (err && typeof err === "object" && "message" in err) return String((err as { message: string }).message); - return "上傳或檢查失敗,請稍後再試。"; + return L("上傳或檢查失敗,請稍後再試。", "Upload or check failed. Try again later."); } export default function ImportBomUpload({ onSuccess }: Props) { + const L = useUiText(); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -54,7 +56,7 @@ const [startTime, setStartTime] = useState(null); const handleUploadAndCheck = async () => { if (files.length === 0) { - setError("請至少選擇一個 .xlsx 檔案"); + setError(L("請至少選擇一個 .xlsx 檔案", "Select at least one .xlsx file")); return; } setLoading(true); @@ -80,7 +82,7 @@ const [startTime, setStartTime] = useState(null); const results = await checkBomFormat(batchId); // 這裡會等後端整個檢查完 onSuccess(batchId, results, files.length); } catch (err: unknown) { - setError(getErrorMessage(err)); + setError(getErrorMessage(err, L)); } finally { setLoading(false); if (timer !== undefined) { @@ -94,14 +96,14 @@ const [startTime, setStartTime] = useState(null); - 選擇 BOM Excel 檔案 + {L("選擇 BOM Excel 檔案", "Choose BOM Excel files")} - 可多選 .xlsx 檔案,或選擇資料夾一次加入多個檔案。 + {L("可多選 .xlsx 檔案,或選擇資料夾一次加入多個檔案。", "You can select several .xlsx files, or choose a folder to add many files at once.")} {files.length > 0 - ? `已選 ${files.length} 個檔案` - : "未選擇"} + ? L(`已選 ${files.length} 個檔案`, `${files.length} files selected`) + : L("未選擇", "None selected")} @@ -132,20 +134,20 @@ const [startTime, setStartTime] = useState(null); onClick={handleUploadAndCheck} disabled={loading || files.length === 0} > - {loading ? "上傳與檢查中…" : "上傳並檢查"} + {loading ? L("上傳與檢查中…", "Uploading and checking…") : L("上傳並檢查", "Upload and check")} {loading && } {loading && progress && ( - 已檢查 {progress.processedFiles} / {progress.totalFiles} 個檔案 - {progress.currentFileName && `,目前:${progress.currentFileName}`} + {L(`已檢查 ${progress.processedFiles} / ${progress.totalFiles} 個檔案`, `Checked ${progress.processedFiles} / ${progress.totalFiles} files`)} + {progress.currentFileName && L(`,目前:${progress.currentFileName}`, `, current: ${progress.currentFileName}`)} {startTime && progress.processedFiles > 0 && progress.totalFiles > 0 && (() => { const elapsedMs = Date.now() - startTime; const avgPerFile = elapsedMs / progress.processedFiles; const remainingFiles = progress.totalFiles - progress.processedFiles; const remainingMs = Math.max(0, remainingFiles * avgPerFile); const remainingSec = Math.round(remainingMs / 1000); - return `,約還需 ${remainingSec} 秒`; + return L(`,約還需 ${remainingSec} 秒`, `, about ${remainingSec} seconds left`); })()} )} diff --git a/src/components/ImportBom/ImportBomWrapper.tsx b/src/components/ImportBom/ImportBomWrapper.tsx index a6d8c520..6f1c7356 100644 --- a/src/components/ImportBom/ImportBomWrapper.tsx +++ b/src/components/ImportBom/ImportBomWrapper.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import React, { useState } from "react"; import Stack from "@mui/material/Stack"; import Tabs from "@mui/material/Tabs"; @@ -11,6 +12,7 @@ import ImportBomDetailTab from "./ImportBomDetailTab"; import type { BomFormatCheckResponse } from "@/app/api/bom"; export default function ImportBomWrapper() { + const L = useUiText(); const [batchId, setBatchId] = useState(null); const [formatResults, setFormatResults] = useState(null); @@ -40,8 +42,8 @@ export default function ImportBomWrapper() { - - + + diff --git a/src/components/InventorySearch/LotLabelPrintModal.tsx b/src/components/InventorySearch/LotLabelPrintModal.tsx index 39ad7047..90259e19 100644 --- a/src/components/InventorySearch/LotLabelPrintModal.tsx +++ b/src/components/InventorySearch/LotLabelPrintModal.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { FLOOR_A_KEY, LOC_PREFIX } from "@/config/demoFloors"; import React, { useCallback, @@ -126,6 +127,7 @@ const LotLabelPrintModal: React.FC = ({ warehouseCodePrefixFilter, hideTriggeredLot, }) => { + const L = useUiText(); const scanInputRef = useRef(null); const [scanInput, setScanInput] = useState(""); const [scanError, setScanError] = useState(null); @@ -177,7 +179,7 @@ const LotLabelPrintModal: React.FC = ({ if (!baseApi) { setSnackbar({ open: true, - message: "NEXT_PUBLIC_API_URL 未設定,無法連線後端。", + message: L("NEXT_PUBLIC_API_URL 未設定,無法連線後端。", "NEXT_PUBLIC_API_URL is not set, so the backend cannot be reached."), severity: "error", }); return; @@ -198,7 +200,7 @@ const LotLabelPrintModal: React.FC = ({ setPrinters([]); setSnackbar({ open: true, - message: e instanceof Error ? e.message : "載入印表機清單失敗", + message: e instanceof Error ? e.message : L("載入印表機清單失敗", "Could not load the printer list"), severity: "error", }); } finally { @@ -241,7 +243,7 @@ const LotLabelPrintModal: React.FC = ({ const analyzePayload = useCallback( async (payload: ScanPayload) => { if (!baseApi) { - setScanError("NEXT_PUBLIC_API_URL 未設定,無法連線後端。"); + setScanError(L("NEXT_PUBLIC_API_URL 未設定,無法連線後端。", "NEXT_PUBLIC_API_URL is not set, so the backend cannot be reached.")); return; } @@ -259,18 +261,18 @@ const LotLabelPrintModal: React.FC = ({ ); if (!res.ok) { const msg = await res.text().catch(() => ""); - throw new Error(msg || `分析失敗(HTTP ${res.status})`); + throw new Error(msg || L(`分析失敗(HTTP ${res.status})`, `Lookup failed (HTTP ${res.status})`)); } const data = (await res.json()) as QrCodeAnalysisResponse; setAnalysis(data); setSnackbar({ open: true, - message: "已載入同品可用批號清單", + message: L("已載入同品可用批號清單", "Loaded available lots for this item"), severity: "success", }); } catch (e) { setAnalysis(null); - setScanError(e instanceof Error ? e.message : "分析失敗"); + setScanError(e instanceof Error ? e.message : L("分析失敗", "Lookup failed")); } finally { setAnalysisLoading(false); } @@ -283,7 +285,7 @@ const LotLabelPrintModal: React.FC = ({ const payload = safeParseScanPayload(raw); if (!payload) { setScanError( - '掃碼內容格式錯誤,需為 JSON,例如 {"itemId":16431,"stockInLineId":10381}', + L('掃碼內容格式錯誤,需為 JSON,例如 {"itemId":16431,"stockInLineId":10381}', 'Scan content must be JSON, for example {"itemId":16431,"stockInLineId":10381}'), ); setAnalysis(null); return; @@ -296,7 +298,7 @@ const LotLabelPrintModal: React.FC = ({ if (!payload) { setSnackbar({ open: true, - message: "請先掃碼或查詢一次,才可刷新批號清單。", + message: L("請先掃碼或查詢一次,才可刷新批號清單。", "Scan or look up once before refreshing the lot list."), severity: "info", }); return; @@ -366,7 +368,7 @@ const LotLabelPrintModal: React.FC = ({ if (!baseApi) { setSnackbar({ open: true, - message: "NEXT_PUBLIC_API_URL 未設定,無法連線後端。", + message: L("NEXT_PUBLIC_API_URL 未設定,無法連線後端。", "NEXT_PUBLIC_API_URL is not set, so the backend cannot be reached."), severity: "error", }); return; @@ -374,7 +376,7 @@ const LotLabelPrintModal: React.FC = ({ if (selectedPrinterId === "") { setSnackbar({ open: true, - message: "請先選擇印表機", + message: L("請先選擇印表機", "Select a printer first"), severity: "error", }); return; @@ -382,7 +384,7 @@ const LotLabelPrintModal: React.FC = ({ if (printQty < 1 || !Number.isFinite(printQty)) { setSnackbar({ open: true, - message: "列印張數需為大於等於 1 的整數", + message: L("列印張數需為大於等於 1 的整數", "Copies to print must be a whole number of at least 1"), severity: "error", }); return; @@ -399,17 +401,17 @@ const LotLabelPrintModal: React.FC = ({ const res = await clientAuthFetch(url, { method: "GET" }); if (!res.ok) { const msg = await res.text().catch(() => ""); - throw new Error(msg || `列印失敗(HTTP ${res.status})`); + throw new Error(msg || L(`列印失敗(HTTP ${res.status})`, `Print failed (HTTP ${res.status})`)); } setSnackbar({ open: true, - message: `已送出列印:Lot ${lotNo}`, + message: L(`已送出列印:Lot ${lotNo}`, `Print sent: lot ${lotNo}`), severity: "success", }); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "列印失敗", + message: e instanceof Error ? e.message : L("列印失敗", "Print failed"), severity: "error", }); } finally { @@ -421,7 +423,7 @@ const LotLabelPrintModal: React.FC = ({ return ( - 批號標籤列印 + {L("批號標籤列印", "Print lot labels")} {statusTitleText ? ( @@ -438,7 +440,7 @@ const LotLabelPrintModal: React.FC = ({ {effectiveHideScanSection ? null : ( <> - 請掃描條碼(JSON 格式),例如{" "} + {L("請掃描條碼(JSON 格式),例如", "Scan a barcode (JSON), for example")}{" "} {'{"itemId":16431,"stockInLineId":10381'}。 @@ -449,13 +451,13 @@ const LotLabelPrintModal: React.FC = ({ > setScanInput(e.target.value)} fullWidth size="small" error={!!scanError} - helperText={scanError || "掃描後按 Enter 或點「查詢」"} + helperText={scanError || L("掃描後按 Enter 或點「查詢」", "After scanning, press Enter or choose Look up")} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); @@ -469,7 +471,7 @@ const LotLabelPrintModal: React.FC = ({ onClick={() => void handleAnalyze()} disabled={analysisLoading || !scanInput.trim()} > - {analysisLoading ? : "查詢"} + {analysisLoading ? : L("查詢", "Look up")} @@ -495,16 +497,16 @@ const LotLabelPrintModal: React.FC = ({ sx={{ minWidth: 260 }} disabled={printersLoading} > - 印表機 + {L("印表機", "Printer")}
- 工單 - 成品 - 狀態 - 產程日 - 批號 - 操作 + {L("工單", "Job order")} + {L("成品", "Finished goods")} + {L("狀態", "Status")} + {L("產程日", "Production date")} + {L("批號", "Lot no.")} + {L("操作", "Actions")} @@ -96,7 +98,7 @@ function AlertTable({ {r.itemName ?? ""} - {statusLabel(r.status)} + {statusLabel(r.status, L)} {fmtProcessDate(r.processDate)} {r.lotNo ?? "—"} @@ -123,7 +125,7 @@ function AlertTable({ endIcon={} onClick={onRowNavigate} > - 上架 + {L("上架", "Put away")} )} @@ -141,6 +143,7 @@ type Props = { /** FP-MTMS Version Checklist | Functions Ref. No. 52 | v1.0.0 | 2026-08-05 */ const JobOrderFgStockInNavAlerts: React.FC = ({ enabled }) => { + const L = useUiText(); const { qcItems, putAwayItems, count, loading, reload } = useJobOrderFgStockInAlerts(enabled); const [open, setOpen] = useState(false); @@ -153,15 +156,15 @@ const JobOrderFgStockInNavAlerts: React.FC = ({ enabled }) => { 0 - ? `點擊查看:待 QC ${qcItems.length}、待上架 ${putAwayItems.length}(與「待 QC 上架」tab 相同:今日往前 ${PRODUCTION_LOOKBACK_DAYS} 天)` - : `今日往前 ${PRODUCTION_LOOKBACK_DAYS} 天內無待 QC/待上架提醒` + ? L(`點擊查看:待 QC ${qcItems.length}、待上架 ${putAwayItems.length}(與「待 QC 上架」tab 相同:今日往前 ${PRODUCTION_LOOKBACK_DAYS} 天)`, `Click to view: pending QC ${qcItems.length}, pending put-away ${putAwayItems.length} (same as the “Pending QC put-away” tab: today, looking back ${PRODUCTION_LOOKBACK_DAYS} days)`) + : L(`今日往前 ${PRODUCTION_LOOKBACK_DAYS} 天內無待 QC/待上架提醒`, `No pending QC or put-away reminders in the last ${PRODUCTION_LOOKBACK_DAYS} days`) } placement="right" > { void reload(); setOpen(true); @@ -194,20 +197,22 @@ const JobOrderFgStockInNavAlerts: React.FC = ({ enabled }) => { - 工單成品:待 QC/待上架 + {L("工單成品:待 QC/待上架", "Job order finished goods: pending QC / put-away")} {loading && ( - 更新中… + {L("更新中…", "Updating…")} )} - 與產程頁「待 QC 上架」相同資料來源:搜索日為今日、往前 - {PRODUCTION_LOOKBACK_DAYS} 天產程;工序全完成、入庫未完成/未拒絕。待 QC:尚未進入已收貨;待上架:已收貨或部分完成入庫。 + {L( + `與產程頁「待 QC 上架」相同資料來源:搜索日為今日、往前 ${PRODUCTION_LOOKBACK_DAYS} 天產程;工序全完成、入庫未完成/未拒絕。待 QC:尚未進入已收貨;待上架:已收貨或部分完成入庫。`, + `Same data as the production page “Pending QC put-away”: the search date is today, looking back ${PRODUCTION_LOOKBACK_DAYS} production days. Every step is complete, and stock-in is unfinished or not rejected. Pending QC: not yet received. Pending put-away: received or stock-in partially completed.`, + )} setOpen(false)} sx={{ position: "absolute", right: 8, top: 8 }} > @@ -217,19 +222,19 @@ const JobOrderFgStockInNavAlerts: React.FC = ({ enabled }) => { {qcItems.length === 0 && putAwayItems.length === 0 && !loading ? ( - 目前沒有符合條件的項目。 + {L("目前沒有符合條件的項目。", "Nothing matches these conditions.")} ) : ( <> {qcItems.length > 0 && putAwayItems.length > 0 && } = ({ enabled }) => { + const L = useUiText(); const { bomGroupCount, itemGroupCount, totalGroupCount } = useMasterDataIssueNavCount(enabled); if (!enabled || totalGroupCount <= 0) return null; - const label = `BOM ${bomGroupCount} 筆 · 貨品 ${itemGroupCount} 筆(共 ${totalGroupCount} 筆)`; + const label = L(`BOM ${bomGroupCount} 筆 · 貨品 ${itemGroupCount} 筆(共 ${totalGroupCount} 筆)`, `BOM ${bomGroupCount} · Items ${itemGroupCount} (${totalGroupCount} total)`); const display = totalGroupCount > 99 ? "99+" : String(totalGroupCount); return ( diff --git a/src/components/NavigationContent/PurchaseStockInNavAlerts.tsx b/src/components/NavigationContent/PurchaseStockInNavAlerts.tsx index 9d6579da..05d01527 100644 --- a/src/components/NavigationContent/PurchaseStockInNavAlerts.tsx +++ b/src/components/NavigationContent/PurchaseStockInNavAlerts.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { usePurchaseStockInAlerts } from "@/hooks/usePurchaseStockInAlerts"; import AssignmentLateIcon from "@mui/icons-material/AssignmentLate"; import CloseIcon from "@mui/icons-material/Close"; @@ -22,10 +23,10 @@ import Typography from "@mui/material/Typography"; import Link from "next/link"; import React, { useState } from "react"; -function statusLabel(s: string | null): string { +function statusLabel(s: string | null, L: (zh: string, en: string) => string): string { const x = (s ?? "").toLowerCase(); - if (x === "pending") return "待處理"; - if (x === "receiving") return "收貨中"; + if (x === "pending") return L("待處理", "Pending"); + if (x === "receiving") return L("收貨中", "Receiving"); return s ?? "—"; } @@ -62,6 +63,7 @@ type Props = { * Sidebar control: opens dialog with recent pending/receiving PO stock-in lines and links to PO edit. */ const PurchaseStockInNavAlerts: React.FC = ({ enabled }) => { + const L = useUiText(); const { items, count, loading, reload } = usePurchaseStockInAlerts(enabled); const [open, setOpen] = useState(false); @@ -72,15 +74,15 @@ const PurchaseStockInNavAlerts: React.FC = ({ enabled }) => { 0 - ? `點擊查看 ${count} 筆近日待完成入庫(待處理/收貨中)並前往處理` - : "近日無待完成採購入庫提醒" + ? L(`點擊查看 ${count} 筆近日待完成入庫(待處理/收貨中)並前往處理`, `Click to view ${count} recent stock-in lines still to finish (pending / receiving) and open them`) + : L("近日無待完成採購入庫提醒", "No recent purchase stock-in reminders") } placement="right" > { void reload(); setOpen(true); @@ -115,19 +117,19 @@ const PurchaseStockInNavAlerts: React.FC = ({ enabled }) => { - 近日待完成採購入庫 + {L("近日待完成採購入庫", "Recent purchase stock-in still to finish")} {loading && ( - 更新中… + {L("更新中…", "Updating…")} )} - 僅列出近幾日建立、狀態為「待處理」或「收貨中」的採購入庫明細。點「前往處理」開啟採購單並定位該明細,可先檢視下方入庫清單再操作。 + {L("僅列出近幾日建立、狀態為「待處理」或「收貨中」的採購入庫明細。點「前往處理」開啟採購單並定位該明細,可先檢視下方入庫清單再操作。", "Lists purchase stock-in lines created in the last few days that are still pending or receiving. Choose “Open to process” to open the purchase order at that line. Review the stock-in list below before you continue.")} setOpen(false)} sx={{ position: "absolute", right: 8, top: 8 }} > @@ -137,23 +139,23 @@ const PurchaseStockInNavAlerts: React.FC = ({ enabled }) => { {count > items.length && items.length > 0 && ( - 共 {count} 筆符合條件,以下顯示最近 {items.length} 筆(可於後端調高單次上限)。 + {L(`共 ${count} 筆符合條件,以下顯示最近 ${items.length} 筆(可於後端調高單次上限)。`, `${count} lines match. Showing the latest ${items.length} (the backend limit can be raised).`)} )} {items.length === 0 && !loading ? ( - 目前沒有符合條件的待完成入庫項目。 + {L("目前沒有符合條件的待完成入庫項目。", "No unfinished stock-in lines match these conditions.")} ) : (
- 採購單 - 物料 - 狀態 - 建立時間 - 批號 - 操作 + {L("採購單", "Purchase order")} + {L("物料", "Material")} + {L("狀態", "Status")} + {L("建立時間", "Created")} + {L("批號", "Lot no.")} + {L("操作", "Actions")} @@ -170,7 +172,7 @@ const PurchaseStockInNavAlerts: React.FC = ({ enabled }) => { {r.itemName ?? ""} - {statusLabel(r.status)} + {statusLabel(r.status, L)} {fmtDt(r.lineCreated)} {r.lotNo ?? "—"} @@ -185,7 +187,7 @@ const PurchaseStockInNavAlerts: React.FC = ({ enabled }) => { endIcon={} onClick={() => setOpen(false)} > - 前往處理 + {L("前往處理", "Open to process")} diff --git a/src/components/PickOrderSearch/EscalationComponent.tsx b/src/components/PickOrderSearch/EscalationComponent.tsx index 5edb2df7..24984b0e 100644 --- a/src/components/PickOrderSearch/EscalationComponent.tsx +++ b/src/components/PickOrderSearch/EscalationComponent.tsx @@ -21,6 +21,7 @@ import { SelectChangeEvent } from '@mui/material/Select'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import { useTranslation } from 'react-i18next'; +import { useUiText } from '@/i18n/useUiText'; interface NameOption { value: string; @@ -44,6 +45,7 @@ const EscalationComponent: React.FC = ({ setIsCollapsed }) => { const { t } = useTranslation("purchaseOrder"); + const L = useUiText(); const [formData, setFormData] = useState({ name: '', @@ -52,12 +54,12 @@ const EscalationComponent: React.FC = ({ }); const nameOptions: NameOption[] = [ - { value: '', label: '請選擇姓名...' }, - { value: 'john', label: '張大明' }, - { value: 'jane', label: '李小美' }, - { value: 'mike', label: '王志強' }, - { value: 'sarah', label: '陳淑華' }, - { value: 'david', label: '林建國' }, + { value: '', label: L('請選擇姓名...', 'Select a name...') }, + { value: 'john', label: L('張大明', 'John') }, + { value: 'jane', label: L('李小美', 'Jane') }, + { value: 'mike', label: L('王志強', 'Mike') }, + { value: 'sarah', label: L('陳淑華', 'Sarah') }, + { value: 'david', label: L('林建國', 'David') }, ]; const handleInputChange = ( @@ -96,7 +98,7 @@ const EscalationComponent: React.FC = ({ } label={ - 上報結果 + {L("上報結果", "Escalation result")} {isCollapsed ? ( ) : ( diff --git a/src/components/PickOrderSearch/PickQcStockInModalVer2.tsx b/src/components/PickOrderSearch/PickQcStockInModalVer2.tsx index 9608cb61..b76e3665 100644 --- a/src/components/PickOrderSearch/PickQcStockInModalVer2.tsx +++ b/src/components/PickOrderSearch/PickQcStockInModalVer2.tsx @@ -22,6 +22,7 @@ import { import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import { dummyQCData, QcData } from "./dummyQcTemplate"; import { submitDialogWithWarning } from "../Swal/CustomAlerts"; @@ -79,6 +80,7 @@ const PickQcStockInModalVer2: React.FC = ({ t, i18n: { language }, } = useTranslation("pickOrder"); + const L = useUiText(); const [qcItems, setQcItems] = useState(dummyQCData) const formProps = useForm({ @@ -126,7 +128,7 @@ const PickQcStockInModalVer2: React.FC = ({ if (validationErrors.length > 0) { console.error("QC Validation failed:", validationErrors); - alert(`未完成品檢: ${validationErrors}`); + alert(`${L("未完成品檢", "QC is incomplete")}: ${validationErrors}`); return; } @@ -150,7 +152,7 @@ const PickQcStockInModalVer2: React.FC = ({ submitDialogWithWarning(() => { console.log("QC accepted with failed items"); onClose?.({} as object, "backdropClick"); - }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); + }, t, {title: L("有不合格檢查項目,確認接受收貨?", "Some checks failed. Accept the goods anyway?"), confirmButtonText: "Confirm", html: ""}); return; } @@ -162,7 +164,7 @@ const PickQcStockInModalVer2: React.FC = ({ onClose?.({} as object, "backdropClick"); } }, - [qcItems, onClose, t], + [L, qcItems, onClose, t], ); const handleQcItemChange = useCallback((index: number, field: keyof QcData, value: any) => { @@ -191,7 +193,10 @@ const PickQcStockInModalVer2: React.FC = ({ GroupA - {itemDetail.pickOrderCode} - 記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標 + {L( + "記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標", + "Record the probe time. Finish goods issue within 1 hour to keep food safe. Checks: visual inspection, smell, and a food thermometer to confirm the temperature meets the standard.", + )} @@ -201,8 +206,8 @@ const PickQcStockInModalVer2: React.FC = ({
- QC模板代號 - 檢查項目 + {L("QC模板代號", "QC template code")} + {L("檢查項目", "Check item")} QC Result Failed Qty Remarks diff --git a/src/components/PickOrderSearch/QCDatagrid.tsx b/src/components/PickOrderSearch/QCDatagrid.tsx index b9947dbb..b1f55588 100644 --- a/src/components/PickOrderSearch/QCDatagrid.tsx +++ b/src/components/PickOrderSearch/QCDatagrid.tsx @@ -33,6 +33,7 @@ import CancelIcon from "@mui/icons-material/Cancel"; import { Add } from "@mui/icons-material"; import { Box, Button, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import { GridApiCommunity, GridSlotsComponentsProps, @@ -113,6 +114,7 @@ function InputDataGrid({ t, // i18n: { language }, } = useTranslation("purchaseOrder"); + const L = useUiText(); const formKey = _formKey.toString(); const { setValue, getValues } = useFormContext(); const [rowModesModel, setRowModesModel] = useState({}); @@ -310,7 +312,7 @@ function InputDataGrid({ onClick={addRow} size="small" > - 新增 + {L("新增", "Add")} {/* {t("Add Record")} */} ); diff --git a/src/components/PickOrderSearch/pickorderModelVer2.tsx b/src/components/PickOrderSearch/pickorderModelVer2.tsx index 2bd870fe..7047b161 100644 --- a/src/components/PickOrderSearch/pickorderModelVer2.tsx +++ b/src/components/PickOrderSearch/pickorderModelVer2.tsx @@ -16,6 +16,7 @@ import { import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import StockInFormVer2 from "./StockInFormVer2"; import QcFormVer2 from "./QcFormVer2"; import PutawayForm from "./PutawayForm"; @@ -77,6 +78,7 @@ const PickQcStockInModalVer2: React.FC = ({ t, i18n: { language }, } = useTranslation("pickOrder"); + const L = useUiText(); const [qcItems, setQcItems] = useState(dummyQCData) const formProps = useForm({ defaultValues: { @@ -159,7 +161,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) if (validationErrors.length > 0) { console.error("QC Validation failed:", validationErrors); - alert(`未完成品檢: ${validationErrors}`); + alert(`${L("未完成品檢", "QC is incomplete")}: ${validationErrors}`); return; } @@ -181,7 +183,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) // await submitQcData(qcData); if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { - submitDialogWithWarning(onOpenPutaway, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); + submitDialogWithWarning(onOpenPutaway, t, {title: L("有不合格檢查項目,確認接受收貨?", "Some checks failed. Accept the goods anyway?"), confirmButtonText: "Confirm", html: ""}); return; } @@ -191,7 +193,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) onClose?.({} as object, "backdropClick"); } }, - [onOpenPutaway, qcItems], + [L, onOpenPutaway, qcItems], ); // Email supplier handler const onSubmitEmailSupplier = useCallback>( diff --git a/src/components/PoDetail/EscalationComponent.tsx b/src/components/PoDetail/EscalationComponent.tsx index bdc60276..44cd3393 100644 --- a/src/components/PoDetail/EscalationComponent.tsx +++ b/src/components/PoDetail/EscalationComponent.tsx @@ -1,3 +1,4 @@ +import { useUiText } from "@/i18n/useUiText"; import React, { useState, ChangeEvent, FormEvent, Dispatch, useEffect } from 'react'; import { Box, @@ -52,6 +53,7 @@ const EscalationComponent: React.FC = ({ // escalationCombo }) => { const { t } = useTranslation("purchaseOrder"); + const L = useUiText(); const [formData, setFormData] = useState({ name: '', @@ -112,7 +114,7 @@ const EscalationComponent: React.FC = ({ {/* */} - 上報結果 + {L("上報結果", "Escalation result")} {/* {isCollapsed ? ( ) : ( @@ -129,7 +131,7 @@ const EscalationComponent: React.FC = ({ } label={ - 上報結果 + {L("上報結果", "Escalation result")} {isCollapsed ? ( ) : ( @@ -160,7 +162,7 @@ const EscalationComponent: React.FC = ({ }} // getOptionLabel={(option) => option.label} options={escalationCombo} - renderInput={(params) => } + renderInput={(params) => } />) }} @@ -190,8 +192,8 @@ const EscalationComponent: React.FC = ({ defaultValue="pass" name="radio-buttons-group" > - } label="合格" /> - } label="不合格" /> + } label={L("合格", "Pass")} /> + } label={L("不合格", "Fail")} /> ) : undefined} @@ -199,23 +201,23 @@ const EscalationComponent: React.FC = ({ fullWidth id="decision" name="decision" - label="上報結果" + label={L("上報結果", "Escalation result")} type="radio" // value={formData.decision} onChange={handleInputChange} InputProps={{ inputProps: { min: 1 } }} - placeholder="請決定上報結果" + placeholder={L("請決定上報結果", "Choose the escalation result")} />)} {/* */} = ({ id="reason" // name="reason" {...register("escalationLog.reason", { - required: "請輸入上報原因", + required: L("請輸入上報原因", "Enter the escalation reason"), })} - label="上報原因" + label={L("上報原因", "Escalation reason")} multiline rows={4} // value={formData.reason} onChange={handleInputChange} - placeholder="請輸入上報原因" + placeholder={L("請輸入上報原因", "Enter the escalation reason")} error={Boolean(errors.escalationLog?.reason)} helperText={errors.escalationLog?.reason?.message} /> diff --git a/src/components/PoDetail/EscalationLog.tsx b/src/components/PoDetail/EscalationLog.tsx index e8788d62..d50de993 100644 --- a/src/components/PoDetail/EscalationLog.tsx +++ b/src/components/PoDetail/EscalationLog.tsx @@ -1,3 +1,4 @@ +import { useUiText } from "@/i18n/useUiText"; import React, { useState } from 'react'; import { Box, @@ -41,6 +42,7 @@ interface QCReportFormProps { } function QCReportForm({ reports }: QCReportFormProps): JSX.Element { + const L = useUiText(); const [selectedReport, setSelectedReport] = useState(null); const handleReportClick = (report: QCReport): void => { @@ -51,7 +53,7 @@ function QCReportForm({ reports }: QCReportFormProps): JSX.Element { return isQualified ? ( } - label="合格" + label={L("合格", "Pass")} color="success" size="small" sx={{ fontWeight: 'medium' }} @@ -59,7 +61,7 @@ function QCReportForm({ reports }: QCReportFormProps): JSX.Element { ) : ( } - label="不合格" + label={L("不合格", "Fail")} color="error" size="small" sx={{ fontWeight: 'medium' }} @@ -79,11 +81,11 @@ function QCReportForm({ reports }: QCReportFormProps): JSX.Element { // ), // }), columnHelper.accessor('isQualified', { - header: '合格', + header: L('合格', 'Pass'), cell: info => getQualifiedStatus(info.getValue()), }), columnHelper.accessor('remarks', { - header: '備註', + header: L('備註', 'Remarks'), cell: info => ( ( {info.getValue()} @@ -115,7 +117,7 @@ function QCReportForm({ reports }: QCReportFormProps): JSX.Element { {/* Table Section */} - 上報資料 + {L("上報資料", "Escalation records")}
@@ -197,7 +199,15 @@ const dummyReports = [ // Main component function EscalationLog(): JSX.Element { - return ; + const L = useUiText(); + const reports = dummyReports.map((row) => ({ + ...row, + remarks: + row.id === "1" + ? L("包裝有破損", "Packaging is damaged") + : L("經檢查,無損壞", "Inspected; no damage"), + })); + return ; }; export default EscalationLog; \ No newline at end of file diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index 403cd0c6..845c46d5 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { fetchPoWithStockInLines, PoResult, @@ -102,6 +103,7 @@ const PoSearchList: React.FC<{ loading?: boolean; }> = ({ poList, selectedPoId, onSelect, loading = false }) => { const { t } = useTranslation(["purchaseOrder", "dashboard"]); + const L = useUiText(); const [searchTerm, setSearchTerm] = useState(''); const filteredPoList = useMemo(() => { @@ -187,8 +189,8 @@ const PoSearchList: React.FC<{ ) : ( {searchTerm.trim() - ? t("No purchase orders match your search", { defaultValue: "沒有符合搜索的採購單" }) - : t("No purchase orders to show", { defaultValue: "沒有可顯示的採購單" })} + ? L("沒有符合搜索的採購單", "No purchase orders match your search") + : L("沒有可顯示的採購單", "No purchase orders to show")} )} @@ -217,6 +219,7 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { }, [session?.user?.abilities]); // console.log(cameras); const { t } = useTranslation("purchaseOrder"); + const L = useUiText(); const apiRef = useGridApiRef(); const [purchaseOrder, setPurchaseOrder] = useState({ ...po }); const [rows, setRows] = useState( @@ -380,16 +383,16 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { const handleBulkPrint = useCallback(async () => { if (!selectedPrinter) { - alert("請先選擇印表機"); + alert(L("請先選擇印表機", "Select a printer first")); return; } if (!Number.isFinite(printQty) || printQty <= 0) { - alert("列印數量必須大於 0"); + alert(L("列印數量必須大於 0", "Print quantity must be greater than 0")); return; } const ids = Array.from(selectedPrintSilIds.values()); if (ids.length <= 0) { - alert("請先選擇要列印的項目"); + alert(L("請先選擇要列印的項目", "Select items to print first")); return; } setIsBulkPrinting(true); @@ -798,7 +801,7 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { - 列印 + {L("列印", "Print")} = ({ po, warehouse, printerCombo }) => { onClick={openPrintDialog} disabled={labelPrinters.length <= 0} > - 選擇列印項目 + {L("選擇列印項目", "Select items to print")} - 只會顯示「待上架 / 已上架」的來貨記錄 + {L("只會顯示「待上架 / 已上架」的來貨記錄", "Only receipt lines that are pending put-away or already put away are shown")} @@ -835,14 +838,14 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { - 列印 + {L("列印", "Print")} @@ -912,7 +915,7 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { - {selectedRow ? `已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}` : "未選擇貨品"} + {selectedRow ? L(`已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}`, `Selected item: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}`) : L("未選擇貨品", "No item selected")} @@ -939,7 +942,7 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { - 列印標籤 + {L("列印標籤", "Print labels")} @@ -952,7 +955,7 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { } /> } - label="待上架" + label={L("待上架", "Pending put-away")} /> = ({ po, warehouse, printerCombo }) => { } /> } - label="已上架" + label={L("已上架", "Put away")} /> = ({ po, warehouse, printerCombo }) => { onChange={(e) => setAllVisiblePrintSilsSelected(e.target.checked)} /> } - label="全選(目前篩選結果)" + label={L("全選(目前篩選結果)", "Select all (current filter)")} /> - 已選擇 {selectedPrintSilIds.size} / {eligiblePrintSils.length} + {L(`已選擇 ${selectedPrintSilIds.size} / ${eligiblePrintSils.length}`, `Selected ${selectedPrintSilIds.size} / ${eligiblePrintSils.length}`)} @@ -991,20 +994,20 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { - 貨品編號 - 貨品名稱 - 換算庫存數量 - 庫存單位 - 收貨日期 - 來貨批號 - 來貨狀態 + {L("貨品編號", "Item code")} + {L("貨品名稱", "Item name")} + {L("換算庫存數量", "Converted stock qty")} + {L("庫存單位", "Stock unit")} + {L("收貨日期", "Receipt date")} + {L("來貨批號", "Receipt lot no.")} + {L("來貨狀態", "Receipt status")} {eligiblePrintSils.map((sil) => { const status = (sil.status ?? "").toLowerCase().trim(); const statusText = - status === "received" ? "待上架" : status === "completed" ? "已上架" : sil.status; + status === "received" ? L("待上架", "Pending put-away") : status === "completed" ? L("已上架", "Put away") : sil.status; const receiptText = sil.receiptDate ? Array.isArray(sil.receiptDate) ? arrayToDateString(sil.receiptDate) @@ -1037,7 +1040,7 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { - 沒有符合條件的項目 + {L("沒有符合條件的項目", "No matching items")} @@ -1049,14 +1052,14 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { diff --git a/src/components/PoDetail/PoDetailRow.tsx b/src/components/PoDetail/PoDetailRow.tsx index d688df85..2f3399ba 100644 --- a/src/components/PoDetail/PoDetailRow.tsx +++ b/src/components/PoDetail/PoDetailRow.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { PurchaseOrderLine } from "@/app/api/po"; import { Box, @@ -92,6 +93,7 @@ export const PoDetailRow = memo(function PoDetailRow({ onSubmitted, }: Props) { const { t } = useTranslation("purchaseOrder"); + const L = useUiText(); const [lotNoInput, setLotNoInput] = useState(savedLotNo); const [dnQtyInput, setDnQtyInput] = useState(savedDnQty); const submitInFlightRef = useRef(false); @@ -109,7 +111,7 @@ export const PoDetailRow = memo(function PoDetailRow({ const acceptedQty = Number(dnQtyInput.trim()); if (!Number.isInteger(acceptedQty) || acceptedQty <= 0) { - alert("來貨數量必須為大於0的整數!"); + alert(L("來貨數量必須為大於0的整數!", "Receipt quantity must be a whole number greater than 0.")); return; } @@ -224,7 +226,7 @@ export const PoDetailRow = memo(function PoDetailRow({ hover title={ needsStockInAttention - ? "採購入庫未完成:此採購明細尚有入庫單為「待處理」或「收貨中」,請於下方完成入庫。" + ? L("採購入庫未完成:此採購明細尚有入庫單為「待處理」或「收貨中」,請於下方完成入庫。", "Purchase stock-in is not finished: this purchase line still has stock-in orders that are pending or receiving. Finish them below.") : undefined } sx={{ @@ -302,7 +304,7 @@ export const PoDetailRow = memo(function PoDetailRow({ ({ @@ -807,7 +809,7 @@ function PoInputGrid({ }, { field: "actions", - headerName: "操作", + headerName: L("操作", "Actions"), width: ACTIONS_COLUMN_WIDTH, minWidth: ACTIONS_COLUMN_WIDTH, flex: 0, diff --git a/src/components/PoDetail/QCDatagrid.tsx b/src/components/PoDetail/QCDatagrid.tsx index 65f108d9..48f41aad 100644 --- a/src/components/PoDetail/QCDatagrid.tsx +++ b/src/components/PoDetail/QCDatagrid.tsx @@ -1,4 +1,5 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { Dispatch, MutableRefObject, @@ -113,6 +114,7 @@ function InputDataGrid({ t, // i18n: { language }, } = useTranslation(["purchaseOrder","dashboard"]); + const L = useUiText(); const formKey = _formKey.toString(); const { setValue, getValues } = useFormContext(); const [rowModesModel, setRowModesModel] = useState({}); @@ -310,7 +312,7 @@ function InputDataGrid({ onClick={addRow} size="small" > - 新增 + {L("新增", "Add")} {/* {t("Add Record")} */} ); diff --git a/src/components/PoDetail/QcStockInModal.tsx b/src/components/PoDetail/QcStockInModal.tsx index 841f3111..fdfe05b9 100644 --- a/src/components/PoDetail/QcStockInModal.tsx +++ b/src/components/PoDetail/QcStockInModal.tsx @@ -1,4 +1,5 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { QcItemWithChecks, QcData } from "@/app/api/qc"; import { Box, @@ -87,6 +88,7 @@ const PoQcStockInModalVer2: React.FC = ({ t, i18n: { language }, } = useTranslation("purchaseOrder"); + const L = useUiText(); const [stockInLineInfo, setStockInLineInfo] = useState(); const [isLoading, setIsLoading] = useState(false); @@ -352,11 +354,11 @@ const PoQcStockInModalVer2: React.FC = ({ // return; // } if (data.expiryDate === undefined || data.expiryDate == null) { - alert("請輸入到期日!"); + alert(L("請輸入到期日!", "Enter the expiry date.")); return; } if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && stockInLineInfo?.status != "escalated") { //TODO: fix it please! - validationErrors.push("有不合格檢查項目,無法收貨!"); + validationErrors.push(L("有不合格檢查項目,無法收貨!", "Some checks failed. Cannot receive this goods.")); // submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?", // confirmButtonText: t("confirm putaway"), html: ""}); // return; @@ -372,7 +374,7 @@ const PoQcStockInModalVer2: React.FC = ({ if (validationErrors.length > 0 && !skipQc) { console.error("QC Validation failed:", validationErrors); - alert(`未完成品檢: ${validationErrors}`); + alert(L(`未完成品檢: ${validationErrors}`, `QC incomplete: ${validationErrors}`)); return; } @@ -404,8 +406,8 @@ const PoQcStockInModalVer2: React.FC = ({ console.log("QC Data for submission:", qcData); if (data.qcDecision == 3) { // Escalate - if (data.escalationLog?.handlerId == undefined) { alert("請選擇上報負責同事!"); return; } - else if (data.escalationLog?.handlerId < 1) { alert("上報負責同事資料有誤"); return; } + if (data.escalationLog?.handlerId == undefined) { alert(L("請選擇上報負責同事!", "Select the colleague responsible for the escalation.")); return; } + else if (data.escalationLog?.handlerId < 1) { alert(L("上報負責同事資料有誤", "The colleague responsible for the escalation is invalid")); return; } const escalationLog = { type : "qc", @@ -430,7 +432,7 @@ const PoQcStockInModalVer2: React.FC = ({ } else { closeHandler({}, "backdropClick"); } - msg("已更新來貨狀態", { + msg(L("已更新來貨狀態", "Receipt status updated"), { position: typeof window !== "undefined" && window.location.pathname.startsWith("/po/edit") @@ -487,11 +489,11 @@ const PoQcStockInModalVer2: React.FC = ({ // return; // } if (data.putAwayLines!!.filter((line) => /[^0-9]/.test(String(line.qty))).length > 0) { //TODO Improve - alert("上架數量不正確!"); + alert(L("上架數量不正確!", "Put-away quantity is invalid.")); return; } if (data.putAwayLines!!.reduce((acc, cur) => acc + Number(cur.qty), 0) > putawayData.acceptQty!!) { - alert(`上架數量不能大於 ${putawayData.acceptQty}!`); + alert(L(`上架數量不能大於 ${putawayData.acceptQty}!`, `Put-away quantity cannot be greater than ${putawayData.acceptQty}.`)); return; } // Handle putaway submission logic here @@ -518,7 +520,7 @@ const PoQcStockInModalVer2: React.FC = ({ try { setIsPrinting(() => true) if ((formProps.watch("putAwayLines") ?? []).filter((line) => /[^0-9]/.test(String(line.printQty))).length > 0) { //TODO Improve - alert("列印數量不正確!"); + alert(L("列印數量不正確!", "Print quantity is invalid.")); return; } // console.log(pafRowSelectionModel) diff --git a/src/components/PoSearch/PoSearch.tsx b/src/components/PoSearch/PoSearch.tsx index 95caf12c..6bae7491 100644 --- a/src/components/PoSearch/PoSearch.tsx +++ b/src/components/PoSearch/PoSearch.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { PoResult } from "@/app/api/po"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -39,6 +40,7 @@ const PoSearch: React.FC = ({ const [filteredPo, setFilteredPo] = useState(po); const [filterArgs, setFilterArgs] = useState>({estimatedArrivalDate : dayjsToDateString(dayjs(), "input")}); const { t } = useTranslation(["purchaseOrder", "common", "dashboard"]); + const L = useUiText(); const router = useRouter(); const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; const [pagingController, setPagingController] = useState( @@ -411,7 +413,7 @@ const PoSearch: React.FC = ({ > - {autoSyncStatus || "載入中..."} + {autoSyncStatus || L("載入中...", "Loading...")} diff --git a/src/components/ProductionProcess/EquipmentStatusDashboard.tsx b/src/components/ProductionProcess/EquipmentStatusDashboard.tsx index 58c48d89..0b9c589d 100644 --- a/src/components/ProductionProcess/EquipmentStatusDashboard.tsx +++ b/src/components/ProductionProcess/EquipmentStatusDashboard.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import React, { useState, useEffect, useCallback } from "react"; import { Box, @@ -130,6 +131,7 @@ const calculateRemainingTime = ( const EquipmentStatusDashboard: React.FC = () => { const { t } = useTranslation(["common", "jo"]); + const L = useUiText(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [tabIndex, setTabIndex] = useState(0); @@ -364,7 +366,7 @@ const EquipmentStatusDashboard: React.FC = () => { - {t("預計完成時間")} + {L("預計完成時間", "Expected finish time")} {details.map((d) => ( diff --git a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx index 096acbb5..7431efcb 100644 --- a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx +++ b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx @@ -1,4 +1,5 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { Box, Button, @@ -64,6 +65,7 @@ const ProductionProcessStepExecution: React.FC { const { t } = useTranslation( ["common","jo","productionProcess"]); + const L = useUiText(); const [lineDetail, setLineDetail] = useState(null); const lineStatusNorm = String(lineDetail?.status ?? "") .trim() @@ -121,6 +123,18 @@ const ProductionProcessStepExecution: React.FC { + const en: Record = { + "千克(KG)": "Kilogram (kg)", + "克(G)": "Gram (g)", + "磅(LB)": "Pound (lb)", + "安士(OZ)": "Ounce (oz)", + "斤(CATTY)": "Catty", + "公升(L)": "Litre (L)", + "毫升(ML)": "Millilitre (ml)", + }; + return L(uom, en[uom] ?? uom); + }; // ✅ 添加:刷新 line detail 的函数 const handleRefreshLineDetail = useCallback(async () => { if (lineId) { @@ -832,7 +846,7 @@ const ProductionProcessStepExecution: React.FC {uomList.map((uom) => ( - {uom} + {uomLabel(uom)} ))} @@ -874,7 +888,7 @@ const ProductionProcessStepExecution: React.FC {uomList.map((uom) => ( - {uom} + {uomLabel(uom)} ))} @@ -922,7 +936,7 @@ const ProductionProcessStepExecution: React.FC {uomList.map((uom) => ( - {uom} + {uomLabel(uom)} ))} @@ -970,7 +984,7 @@ const ProductionProcessStepExecution: React.FC {uomList.map((uom) => ( - {uom} + {uomLabel(uom)} ))} @@ -1018,7 +1032,7 @@ const ProductionProcessStepExecution: React.FC {uomList.map((uom) => ( - {uom} + {uomLabel(uom)} ))} diff --git a/src/components/PutAwayScan/PutAwayModal.tsx b/src/components/PutAwayScan/PutAwayModal.tsx index 1cfdaac8..f2205519 100644 --- a/src/components/PutAwayScan/PutAwayModal.tsx +++ b/src/components/PutAwayScan/PutAwayModal.tsx @@ -30,6 +30,7 @@ import { WarehouseResult } from "@/app/api/warehouse"; // import { QrCodeInfo } from "@/app/api/qrcde"; import { Check, QrCode, ErrorOutline, CheckCircle } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import { useSearchParams } from "next/navigation"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import LoadingComponent from "../General/LoadingComponent"; @@ -86,6 +87,7 @@ const scannerStyle = { const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId, warehouseId, scanner, addPutAwayHistory, onSetDefaultWarehouseId, onScanPhaseChange }) => { const { t } = useTranslation("putAway"); + const L = useUiText(); const [serverError, setServerError] = useState(""); const params = useSearchParams(); @@ -120,12 +122,12 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId const getRejectMessage = (status: string): string => { switch (status) { - case "pending": return "此貨品有待品檢"; - case "rejected": return "此貨品已被拒收"; - case "escalated": return "此貨品已被上報"; - case "partially_completed": return "此貨品已部分上架"; - case "completed": return "此貨品已上架"; - default: return "此貨品暫時無法上架"; + case "pending": return L("此貨品有待品檢", "This item is waiting for quality check"); + case "rejected": return L("此貨品已被拒收", "This item was rejected"); + case "escalated": return L("此貨品已被上報", "This item was escalated"); + case "partially_completed": return L("此貨品已部分上架", "This item is partly put away"); + case "completed": return L("此貨品已上架", "This item is already put away"); + default: return L("此貨品暫時無法上架", "This item cannot be put away right now"); } }; @@ -241,14 +243,14 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId if (warehouseId !== firstWarehouseId) { const firstWh = warehouse.find((w) => w.id == firstWarehouseId); const scannedWh = warehouse.find((w) => w.id == warehouseId); - setWarehouseMismatchError("倉庫不匹配!必須使用首次上架的倉庫"); + setWarehouseMismatchError(L("倉庫不匹配!必須使用首次上架的倉庫", "Warehouse does not match. Use the warehouse from the first put-away.")); setVerified(false); } else { setWarehouseMismatchError(""); if (scanner.isScanning) { setIsOpenScanner(false); setVerified(true); - msg("貨倉掃瞄成功!"); + msg(L("貨倉掃瞄成功!", "Warehouse scanned.")); scannerRef.current.resetScan(); } } @@ -257,7 +259,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId if (scanner.isScanning) { setIsOpenScanner(false); setVerified(true); - msg("貨倉掃瞄成功!"); + msg(L("貨倉掃瞄成功!", "Warehouse scanned.")); scannerRef.current.resetScan(); } } @@ -347,7 +349,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId setWarehouseScanPhase(false); resetModalState(); onCloseRef.current(true); - msgError("錯誤的二維碼"); + msgError(L("錯誤的二維碼", "Invalid QR code")); } }, [resetModalState, setWarehouseScanPhase]); @@ -399,7 +401,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId // } try { if (!itemDetail?.id || itemDetail.acceptedQty === undefined || itemDetail.acceptedQty === null) { - alert("找不到收貨數量(acceptedQty),請重新掃碼再試。"); + alert(L("找不到收貨數量(acceptedQty),請重新掃碼再試。", "Accepted quantity was not found. Scan the code again.")); return; } // 确定最终使用的 warehouseId @@ -408,7 +410,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId : (itemDefaultWarehouseId || 0); if (firstWarehouseId !== null && effectiveWarehouseId !== firstWarehouseId) { - setWarehouseMismatchError("倉庫不匹配!必須使用首次上架的倉庫"); + setWarehouseMismatchError(L("倉庫不匹配!必須使用首次上架的倉庫", "Warehouse does not match. Use the warehouse from the first put-away.")); return; } const args = { @@ -468,7 +470,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId addPutAwayHistory(putAwayData); - msg("貨品上架成功!"); + msg(L("貨品上架成功!", "Item put away.")); closeHandler({}, "backdropClick"); } @@ -498,7 +500,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId <> - 處理上架 + {L("處理上架", "Put away")} @@ -549,7 +551,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId }} noWrap > - 掃瞄完成 + {L("掃瞄完成", "Scan complete")} ) : ( @@ -571,8 +573,8 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId noWrap > {warehouseMismatchError || (firstWarehouseId !== null && warehouseId > 0 && warehouseId !== firstWarehouseId) - ? "倉庫不匹配!請掃瞄首次上架的倉庫" - : "請掃瞄倉庫二維碼"} + ? L("倉庫不匹配!請掃瞄首次上架的倉庫", "Warehouse does not match. Scan the warehouse from the first put-away.") + : L("請掃瞄倉庫二維碼", "Scan the warehouse QR code")} ) @@ -590,7 +592,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId }} noWrap > - {warehouseDisplay} {verified ? "" : `(建議)`} + {warehouseDisplay} {verified ? "" : L("(建議)", "(suggested)")} diff --git a/src/components/Qc/QcComponent.tsx b/src/components/Qc/QcComponent.tsx index 3c111f7d..783c3891 100644 --- a/src/components/Qc/QcComponent.tsx +++ b/src/components/Qc/QcComponent.tsx @@ -1,5 +1,6 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { Box, Card, CardContent, Checkbox, Collapse, FormControl, FormControlLabel, Grid, Radio, RadioGroup, Stack, Tab, @@ -68,6 +69,7 @@ type QcRow = TableRow, EntryError>; // fetchQcItemCheck const QcComponent: React.FC = ({ itemDetail, disabled = false, compactLayout = false, denseLayout = false }) => { const { t } = useTranslation("purchaseOrder"); + const L = useUiText(); const apiRef = useGridApiRef(); const { register, @@ -188,7 +190,7 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false, compactLay `${t("QC items without result")}`)) return; if (validateFieldFail("qcDecision", (!qcResultItems.every((qc) => qc.qcPassed) && qcDecision == 1 && itemDetail.status != "escalated"), - "有不合格檢查項目,無法收貨!")) return; // TODO: Fix it please + L("有不合格檢查項目,無法收貨!", "Some checks failed. Cannot receive this goods."))) return; // TODO: Fix it please } // console.log("Validated without errors"); @@ -447,7 +449,7 @@ useEffect(() => { {qcCategory?.name} ({qcCategory?.code}) - 品檢類型:{qcType} + {L("品檢類型", "QC type")}:{qcType} {formattedDesc(qcCategory?.description)} @@ -455,7 +457,7 @@ useEffect(() => { ); - }, [qcType, qcCategory, headerVariant, subVariant, sub2Variant, denseLayout]); + }, [qcType, qcCategory, headerVariant, subVariant, sub2Variant, denseLayout, L]); return ( <> @@ -571,7 +573,7 @@ useEffect(() => { }} > } label="接受來貨" /> + value="1" control={} label={L("接受來貨", "Accept receipt")} /> {(detailMode || (disabled && accQty != itemDetail.acceptedQty && qcDecision == 1)) && ( //TODO Improve @@ -647,13 +649,13 @@ useEffect(() => { } sx={{"& .Mui-checked": {color: "red"}}} - label= {detailMode ? "全部拒絕並退貨" : "不接受並需要退貨"} /> + label={detailMode ? L("全部拒絕並退貨", "Reject all and return") : L("不接受並需要退貨", "Reject and return")} /> {(itemDetail.status == "pending" || disabled) && (<> } sx={{"& .Mui-checked": {color: "blue"}}} - label="暫時存放到置物區,並等待品檢結果" /> + label={L("暫時存放到置物區,並等待品檢結果", "Hold in the storage area and wait for the QC result")} /> )} diff --git a/src/components/Qc/QcForm.tsx b/src/components/Qc/QcForm.tsx index ab90d2b1..61fdc672 100644 --- a/src/components/Qc/QcForm.tsx +++ b/src/components/Qc/QcForm.tsx @@ -18,6 +18,7 @@ import { import { QcFormInput, QcResult } from "@/app/api/qc"; import { isMeasurableQcItem, resolveMeasureMeta } from "@/app/api/qc/measurement"; import { QC_MEASUREMENT_ENABLED } from "@/config/featureFlags"; +import { useUiText } from "@/i18n/useUiText"; interface Props { rows: QcResult[]; @@ -27,7 +28,8 @@ interface Props { } const QcForm: React.FC = ({ rows, disabled = false, denseLayout = false }) => { - const { t } = useTranslation("purchaseOrder"); + const { t, i18n } = useTranslation("purchaseOrder"); + const L = useUiText(); const apiRef = useGridApiRef(); const { register, @@ -112,7 +114,7 @@ const QcForm: React.FC = ({ rows, disabled = false, denseLayout = false } } - label="合格" + label={L("合格", "Accept")} disabled={qcDisabled(rowValue)} sx={{ color: rowValue.qcPassed === true ? "green" : "inherit", @@ -122,7 +124,7 @@ const QcForm: React.FC = ({ rows, disabled = false, denseLayout = false } } - label="不合格" + label={L("不合格", "Deny")} disabled={qcDisabled(rowValue)} sx={{ color: rowValue.qcPassed === false ? "red" : "inherit", @@ -262,7 +264,7 @@ const QcForm: React.FC = ({ rows, disabled = false, denseLayout = false } ); }, }, - ], [denseLayout, t]) + ], [denseLayout, t, i18n.language]) // const getRowId = (row :any) => { // return qcRecord.findIndex(qc => qc == row); diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx index 719cf04e..be8e66c3 100644 --- a/src/components/Qc/QcStockInModal.tsx +++ b/src/components/Qc/QcStockInModal.tsx @@ -1,4 +1,5 @@ "use client"; +import { useUiText } from "@/i18n/useUiText"; import { QcItemWithChecks, QcData } from "@/app/api/qc"; import { Box, @@ -91,6 +92,7 @@ const QcStockInModal: React.FC = ({ t, i18n: { language }, } = useTranslation("purchaseOrder"); + const L = useUiText(); const useCompactFields = useMemo(() => { return uiMode === "dashboard" || uiMode === "productionProcess"; }, [uiMode]); @@ -405,11 +407,11 @@ const QcStockInModal: React.FC = ({ // return; // } if (data.expiryDate === undefined || data.expiryDate == null) { - alert("請輸入到期日!"); + alert(L("請輸入到期日!", "Enter the expiry date.")); return; } if (!qcResultsLocal.every((qc) => qc.qcPassed) && qcAcceptLocal && stockInLineInfo?.status != StockInStatus.ESCALATED) { //TODO: fix it please! - validationErrors.push("有不合格檢查項目,無法收貨!"); + validationErrors.push(L("有不合格檢查項目,無法收貨!", "Some checks failed. Cannot receive this goods.")); // submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?", // confirmButtonText: t("confirm putaway"), html: ""}); // return; @@ -425,7 +427,7 @@ const QcStockInModal: React.FC = ({ if (validationErrors.length > 0 && !skipQc) { console.error("QC Validation failed:", validationErrors); - alert(`未完成品檢: ${validationErrors}`); + alert(L(`未完成品檢: ${validationErrors}`, `QC incomplete: ${validationErrors}`)); return; } @@ -439,7 +441,7 @@ const QcStockInModal: React.FC = ({ ); const willAcceptStock = Boolean(skipQc) || qcAcceptLocal; if (willAcceptStock && needsStockQtyRound) { - alert("請先在換算庫存數量選擇向上或向下取整"); + alert(L("請先在換算庫存數量選擇向上或向下取整", "Choose round up or round down for the converted stock quantity first")); return; } @@ -478,8 +480,8 @@ const QcStockInModal: React.FC = ({ let qcRes: StockInLine | undefined; if (data.qcDecision == 3) { // Escalate - if (data.escalationLog?.handlerId == undefined) { alert("請選擇上報負責同事!"); return; } - else if (data.escalationLog?.handlerId < 1) { alert("上報負責同事資料有誤"); return; } + if (data.escalationLog?.handlerId == undefined) { alert(L("請選擇上報負責同事!", "Select the colleague responsible for the escalation.")); return; } + else if (data.escalationLog?.handlerId < 1) { alert(L("上報負責同事資料有誤", "The colleague responsible for the escalation is invalid")); return; } const escalationLog = { type : "qc", @@ -590,7 +592,7 @@ const QcStockInModal: React.FC = ({ } setIsSubmitting(false); qcSubmitInFlightRef.current = false; - msg("已更新來貨狀態", { + msg(L("已更新來貨狀態", "Receipt status updated"), { position: typeof window !== "undefined" && window.location.pathname.startsWith("/po/edit") @@ -656,11 +658,11 @@ const QcStockInModal: React.FC = ({ // return; // } if (data.putAwayLines!!.filter((line) => /[^0-9]/.test(String(line.qty))).length > 0) { //TODO Improve - alert("上架數量不正確!"); + alert(L("上架數量不正確!", "Put-away quantity is invalid.")); return; } if (data.putAwayLines!!.reduce((acc, cur) => acc + Number(cur.qty), 0) > putawayData.acceptQty!!) { - alert(`上架數量不能大於 ${putawayData.acceptQty}!`); + alert(L(`上架數量不能大於 ${putawayData.acceptQty}!`, `Put-away quantity cannot be greater than ${putawayData.acceptQty}.`)); return; } // Handle putaway submission logic here @@ -699,7 +701,7 @@ const QcStockInModal: React.FC = ({ try { setIsPrinting(() => true) if ((formProps.watch("putAwayLines") ?? []).filter((line) => /[^0-9]/.test(String(line.printQty))).length > 0) { //TODO Improve - alert("列印數量不正確!"); + alert(L("列印數量不正確!", "Print quantity is invalid.")); return; } // Conditionally call different APIs based on source diff --git a/src/components/Shop/RouteBoard.tsx b/src/components/Shop/RouteBoard.tsx index 5e86b347..c36efcb2 100644 --- a/src/components/Shop/RouteBoard.tsx +++ b/src/components/Shop/RouteBoard.tsx @@ -1,6 +1,7 @@ "use client"; import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; +import { useUiText } from "@/i18n/useUiText"; import React, { useCallback, useEffect, @@ -795,6 +796,7 @@ function dedupeShopMasterRows( */ const RouteBoard: React.FC = () => { const { t } = useTranslation("shop"); + const L = useUiText(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [laneWarnDrawerOpen, setLaneWarnDrawerOpen] = useState(false); @@ -6812,7 +6814,9 @@ const RouteBoard: React.FC = () => { lineHeight: 1.25, }} > - {company} + {company === "未分配物流商" + ? L("未分配物流商", "Unassigned logistics provider") + : company} {canManage && ( { lineHeight: 1.25, }} > - {company} + {company === "未分配物流商" + ? L("未分配物流商", "Unassigned logistics provider") + : company} {columnHasDirtyLogistics && ( { }} noWrap > - {district} + {district === "未分類" + ? L("未分類", "Unclassified") + : district} = ({ textfieldSx, }) => { const { t, i18n: { language }, } = useTranslation("purchaseOrder"); + const L = useUiText(); const fieldSx = { ...(textfieldSx ?? defaultModalFieldSx), "& .MuiInputLabel-root": { display: "none" }, @@ -291,9 +293,9 @@ const CalculateExpiryDateModal: React.FC = ({ - 年 - 月 - 日 + {L("年", "Year")} + {L("月", "Month")} + {L("日", "Day")} @@ -394,7 +396,7 @@ const CalculateExpiryDateModal: React.FC = ({ > {t("shelfLife")}:{" "} - {shelfLife ?? 0} 日 + {L(`${shelfLife ?? 0} 日`, `${shelfLife ?? 0} days`)} diff --git a/src/components/StockIn/ShelfLifeInput.tsx b/src/components/StockIn/ShelfLifeInput.tsx index 978ba99d..c942bf38 100644 --- a/src/components/StockIn/ShelfLifeInput.tsx +++ b/src/components/StockIn/ShelfLifeInput.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useMemo } from 'react'; import { Box, TextField, FormHelperText, Typography, styled } from '@mui/material'; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; interface ShelfLifeInputProps { value?: number; // Total days @@ -61,7 +62,12 @@ const durationToDays = (years: number, months: number, days: number) => { }; // Format duration to readable string with proper roll-over (Assuming 30 days per month) -const formatDuration = (years: number, months: number, days: number) => { +const formatDuration = ( + years: number, + months: number, + days: number, + unit: { year: string; month: string; day: string }, +) => { let adjustedYears = years; let adjustedMonths = months; let adjustedDays = days; @@ -79,10 +85,10 @@ const formatDuration = (years: number, months: number, days: number) => { } const parts: string[] = []; - if (adjustedYears > 0) parts.push(`${adjustedYears} 年`); - if (adjustedMonths > 0) parts.push(`${adjustedMonths} 個月 ${adjustedDays > 0 ? ' + ' : ''}`); - if (adjustedDays > 0) parts.push(`${adjustedDays} 日`); - return parts.length > 0 ? parts.join(' ') : '0 日'; + if (adjustedYears > 0) parts.push(`${adjustedYears} ${unit.year}`); + if (adjustedMonths > 0) parts.push(`${adjustedMonths} ${unit.month}${adjustedDays > 0 ? " +" : ""}`); + if (adjustedDays > 0) parts.push(`${adjustedDays} ${unit.day}`); + return parts.length > 0 ? parts.join(" ") : `0 ${unit.day}`; }; const ShelfLifeInput: React.FC = ({ @@ -96,6 +102,12 @@ const ShelfLifeInput: React.FC = ({ inputsOnly = false, }) => { const { t } = useTranslation("purchaseOrder"); + const L = useUiText(); + const unit = { + year: L("年", "Year"), + month: L("月", "Month"), + day: L("日", "Day"), + }; const { years, months, days } = daysToDuration(value); const [duration, setDuration] = useState({ @@ -146,7 +158,7 @@ const ShelfLifeInput: React.FC = ({ > {label}:{' '} - {totalDays} 日 + {totalDays} {unit.day} ); @@ -155,7 +167,7 @@ const ShelfLifeInput: React.FC = ({ const yearField = ( = ({ const monthField = ( = ({ const dayField = ( = ({ const inputsRow = ( - {!inputsOnly && externalLabels && 年} + {!inputsOnly && externalLabels && {unit.year}} {yearField} {!inputsOnly && helperUnderYear && shelfLifeSummary} - {!inputsOnly && externalLabels && 月} + {!inputsOnly && externalLabels && {unit.month}} {monthField} - {!inputsOnly && externalLabels && 日} + {!inputsOnly && externalLabels && {unit.day}} {dayField} diff --git a/src/components/StockIn/StockInForm.tsx b/src/components/StockIn/StockInForm.tsx index 3de51d0d..c9c3ddfc 100644 --- a/src/components/StockIn/StockInForm.tsx +++ b/src/components/StockIn/StockInForm.tsx @@ -15,6 +15,7 @@ import { } from "@mui/material"; import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import StyledDataGrid from "../StyledDataGrid"; import { useCallback, useEffect, useState } from "react"; import { @@ -83,6 +84,7 @@ const StockInForm: React.FC = ({ t, i18n: { language }, } = useTranslation("purchaseOrder"); + const L = useUiText(); const apiRef = useGridApiRef(); const { register, @@ -374,7 +376,7 @@ const StockInForm: React.FC = ({ <> = ({ = ({ = ({ users }) => { const { t } = useTranslation(["user", "common"]); + const L = useUiText(); const router = useRouter(); const [savedUsers, setSavedUsers] = useState(() => cloneUserList(users)); const [allUsers, setAllUsers] = useState(() => cloneUserList(users)); @@ -192,10 +194,10 @@ const UserExcelSheetView: React.FC = ({ users }) => { const searchCriteria: Criterion[] = useMemo( () => [ - { label: "用戶/姓名", paramName: "username", type: "text" }, + { label: L("用戶/姓名", "User / name"), paramName: "username", type: "text" }, { label: t("staffNo"), paramName: "staffNo", type: "text" }, ], - [t], + [t, L], ); const authorityColumns = useMemo(() => { @@ -330,9 +332,7 @@ const UserExcelSheetView: React.FC = ({ users }) => { setAllUsers(cloneUserList(savedUsers)); const msg = error instanceof Error ? error.message : String(error); - let text = t("Save failed. Please try again.", { - defaultValue: "儲存失敗,請再試一次。", - }); + let text = L("儲存失敗,請再試一次。", "Save failed. Please try again."); if (msg.includes("USERNAME_NOT_AVAILABLE")) { text = t("Username is already taken"); } else if (msg.includes("USER_WRONG_NEW_PWD")) { @@ -355,7 +355,7 @@ const UserExcelSheetView: React.FC = ({ users }) => { setIsSaving(false); saveInFlightRef.current = false; } - }, [allUsers, authorityIds, hasPendingChanges, router, savedUsers, t]); + }, [L, allUsers, authorityIds, hasPendingChanges, router, savedUsers, t]); return ( <> @@ -374,7 +374,7 @@ const UserExcelSheetView: React.FC = ({ users }) => { onClick={handleSave} disabled={isSaving} > - {isSaving ? t("Saving...", { defaultValue: "儲存中..." }) : t("Save", { ns: "common" })} + {isSaving ? L("儲存中...", "Saving...") : t("Save", { ns: "common" })} )} @@ -385,7 +385,7 @@ const UserExcelSheetView: React.FC = ({ users }) => { - 編輯 & 員工資訊 + {L("編輯 & 員工資訊", "Edit and employee info")} {authorityColumns.map(authority => ( = ({ users }) => { setPage(0); }} rowsPerPageOptions={[10, 20, 50]} - labelRowsPerPage="每頁" + labelRowsPerPage={L("每頁", "Rows per page")} /> diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx index a3062785..bba21e2d 100644 --- a/src/components/UserSearch/UserSearch.tsx +++ b/src/components/UserSearch/UserSearch.tsx @@ -3,6 +3,7 @@ import SearchBox, { Criterion } from "../SearchBox"; import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; import DeleteIcon from "@mui/icons-material/Delete"; @@ -25,6 +26,7 @@ type SearchParamNames = keyof SearchQuery; const UserSearch: React.FC = ({ users }) => { const { t } = useTranslation("user"); + const L = useUiText(); const [filteredUser, setFilteredUser] = useState(users); const [pagingController, setPagingController] = useState({ pageNum: 1, @@ -37,7 +39,7 @@ const UserSearch: React.FC = ({ users }) => { const searchCriteria: Criterion[] = useMemo( () => [ { - label: "用戶/姓名", + label: L("用戶/姓名", "User / name"), paramName: "username", type: "text", }, @@ -47,7 +49,7 @@ const UserSearch: React.FC = ({ users }) => { type: "text", }, ], - [t], + [t, L], ); const onUserClick = useCallback( diff --git a/src/components/Warehouse/WarehouseHandle.tsx b/src/components/Warehouse/WarehouseHandle.tsx index 59b6ed3f..fc0255b0 100644 --- a/src/components/Warehouse/WarehouseHandle.tsx +++ b/src/components/Warehouse/WarehouseHandle.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import SearchResults, { Column } from "../SearchResults/SearchResults"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; @@ -33,6 +34,7 @@ type SearchParamNames = keyof SearchQuery; const WarehouseHandle: React.FC = ({ warehouses }) => { const { t } = useTranslation(["warehouse", "common"]); + const L = useUiText(); const [filteredWarehouse, setFilteredWarehouse] = useState(warehouses); const [pagingController, setPagingController] = useState({ pageNum: 1, @@ -112,12 +114,12 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { const sectionPattern = /^[A-Za-z0-9]{2}-[A-Za-z0-9]{3}$/; if (trimmedOrder && !orderPattern.test(trimmedOrder)) { - setEditError(`${t("order")} 格式必須為 XF-YYY`); + setEditError(L(`${t("order")} 格式必須為 XF-YYY`, `${t("order")} must use the format XF-YYY`)); return; } if (trimmedStockTakeSection && !sectionPattern.test(trimmedStockTakeSection)) { - setEditError(`${t("stockTakeSection")} 格式必須為 ST-YYY`); + setEditError(L(`${t("stockTakeSection")} 格式必須為 ST-YYY`, `${t("stockTakeSection")} must use the format ST-YYY`)); return; } @@ -153,7 +155,7 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { } finally { setIsSavingEdit(false); } - }, [editValues, editingWarehouse, router, t, setFilteredWarehouse]); + }, [L, editValues, editingWarehouse, router, t, setFilteredWarehouse]); const handleSearch = useCallback(() => { setIsSearching(true); diff --git a/src/components/WarehouseHandle/WarehouseHandle.tsx b/src/components/WarehouseHandle/WarehouseHandle.tsx index fbac9d93..ac77ade9 100644 --- a/src/components/WarehouseHandle/WarehouseHandle.tsx +++ b/src/components/WarehouseHandle/WarehouseHandle.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import SearchResults, { Column } from "../SearchResults/SearchResults"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; @@ -33,6 +34,7 @@ type SearchParamNames = keyof SearchQuery; const WarehouseHandle: React.FC = ({ warehouses }) => { const { t } = useTranslation(["warehouse", "common"]); + const L = useUiText(); const [filteredWarehouse, setFilteredWarehouse] = useState(warehouses); const [pagingController, setPagingController] = useState({ pageNum: 1, @@ -110,12 +112,12 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { const sectionPattern = /^[A-Za-z0-9]{2}-[A-Za-z0-9]{3}$/; if (trimmedOrder && !orderPattern.test(trimmedOrder)) { - setEditError(`${t("order")} 格式必須為 XF-YYY`); + setEditError(L(`${t("order")} 格式必須為 XF-YYY`, `${t("order")} must use the format XF-YYY`)); return; } if (trimmedStockTakeSection && !sectionPattern.test(trimmedStockTakeSection)) { - setEditError(`${t("stockTakeSection")} 格式必須為 ST-YYY`); + setEditError(L(`${t("stockTakeSection")} 格式必須為 ST-YYY`, `${t("stockTakeSection")} must use the format ST-YYY`)); return; } @@ -149,7 +151,7 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { } finally { setIsSavingEdit(false); } - }, [editValues, editingWarehouse, router, t, setFilteredWarehouse]); + }, [L, editValues, editingWarehouse, router, t, setFilteredWarehouse]); const handleSearch = useCallback(() => { setIsSearching(true); diff --git a/src/components/charts/SafeApexCharts.tsx b/src/components/charts/SafeApexCharts.tsx index 0e41126b..528cafd4 100644 --- a/src/components/charts/SafeApexCharts.tsx +++ b/src/components/charts/SafeApexCharts.tsx @@ -5,6 +5,7 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import type { ApexOptions } from "apexcharts"; import type { Props as ApexChartProps } from "react-apexcharts"; +import { useUiText } from "@/i18n/useUiText"; export type SafeApexChartsProps = ApexChartProps & { /** Bumps internal remount when set — JSON.stringify(options) drops `chart.events`, so use this when handlers/data must stay in sync. */ @@ -148,8 +149,6 @@ function buildApexConfig( }; } -const EMPTY_MESSAGE = "暫無圖表資料(後端無法連線或此區間無資料)。"; - /** Apex mutates `options` in place (circular refs). Never throw during render. */ function safeStringify(value: unknown): string { const seen = new WeakSet(); @@ -179,6 +178,7 @@ function destroyChart(chartRef: { current: { destroy: () => void } | null }) { } export default function SafeApexCharts(props: SafeApexChartsProps) { + const L = useUiText(); const { type, series, options, height, width, chartRevision, allowZeroSeries = false, ...rest } = props; const containerRef = useRef(null); const chartRef = useRef<{ destroy: () => void } | null>(null); @@ -199,7 +199,7 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { const pairs = sanitized.map((v, i) => { const n = Number.isFinite(v) ? v : 0; const raw = prev[i]; - const label = raw != null && String(raw).trim() !== "" ? String(raw) : `項目 ${i + 1}`; + const label = raw != null && String(raw).trim() !== "" ? String(raw) : L("項目", "Item") + ` ${i + 1}`; return { v: Math.max(0, n), label }; }); const nonzero = pairs.filter((p) => p.v > 0); @@ -273,7 +273,10 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { if (showPlaceholder) { return ( - {EMPTY_MESSAGE} + {L( + "暫無圖表資料(後端無法連線或此區間無資料)。", + "No chart data (the server is unreachable, or this range has no data).", + )} ); } diff --git a/src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx b/src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx index 69c2ae96..3c985145 100644 --- a/src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx +++ b/src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx @@ -3,6 +3,7 @@ import SearchBox, { Criterion } from "../SearchBox"; import { useCallback, useMemo, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import SearchResults, { Column } from "../SearchResults"; import { useRouter } from "next/navigation"; import { successDialog } from "../Swal/CustomAlerts"; @@ -46,6 +47,7 @@ type SearchParamNames = keyof SearchQuery; const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printerCombo }) => { const { t } = useTranslation("common"); + const L = useUiText(); const [filteredEquipmentDetails, setFilteredEquipmentDetails] = useState([]); const router = useRouter(); const { setIsUploading } = useUploadContext(); @@ -86,17 +88,17 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe const searchCriteria: Criterion[] = useMemo( () => [ { - label: "設備名稱", + label: L("設備名稱", "Equipment name"), paramName: "code", type: "text", }, { - label: "設備編號", + label: L("設備編號", "Equipment no."), paramName: "equipmentCode", type: "text", }, ], - [], + [L], ); interface ApiResponse { @@ -267,13 +269,13 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe const response = await exportEquipmentQrCode(numericIds); downloadFile(response.blobValue, response.filename); setSelectedEquipmentDetailsModalOpen(false); - successDialog("二維碼已下載", t); + successDialog(L("二維碼已下載", "QR code downloaded"), t); } catch (error) { console.error("Error exporting QR code:", error); } finally { setIsUploading(false); } - }, [setIsUploading, t]); + }, [L, setIsUploading, t]); const handlePrint = useCallback(async () => { if (checkboxIds.length === 0 || !selectedPrinter) { @@ -288,13 +290,13 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe printQty, }); setSelectedEquipmentDetailsModalOpen(false); - successDialog("二維碼已列印", t); + successDialog(L("二維碼已列印", "QR code printed"), t); } catch (error) { console.error("Error printing QR code:", error); } finally { setIsUploading(false); } - }, [checkboxIds, printQty, selectedPrinter, setIsUploading, t]); + }, [L, checkboxIds, printQty, selectedPrinter, setIsUploading, t]); const handleViewSelectedQrCodes = useCallback(() => { if (checkboxIds.length === 0) { @@ -327,27 +329,27 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe }, { name: "code", - label: "設備名稱", + label: L("設備名稱", "Equipment name"), align: "left", headerAlign: "left", sx: { width: "150px", minWidth: "150px" }, }, { name: "description", - label: "設備描述", + label: L("設備描述", "Equipment description"), align: "left", headerAlign: "left", sx: { width: "200px", minWidth: "200px" }, }, { name: "equipmentCode", - label: "設備編號", + label: L("設備編號", "Equipment no."), align: "left", headerAlign: "left", sx: { width: "150px", minWidth: "150px" }, }, ], - [t, checkboxIds, handleSelectEquipmentDetail], + [L, checkboxIds, handleSelectEquipmentDetail], ); const onReset = useCallback(() => { @@ -382,7 +384,10 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe startIcon={} disabled={isSearching || totalCount === 0} > - 選擇全部設備 ({checkboxIds.length} / {totalCount}) + {L( + `選擇全部設備 (${checkboxIds.length} / ${totalCount})`, + `Select all equipment (${checkboxIds.length} / ${totalCount})`, + )} @@ -425,7 +433,10 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe }} > - 已選擇設備 ({selectedEquipmentDetails.length}) + {L( + `已選擇設備 (${selectedEquipmentDetails.length})`, + `Selected equipment (${selectedEquipmentDetails.length})`, + )} @@ -444,13 +455,13 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe - 設備名稱 + {L("設備名稱", "Equipment name")} - 設備描述 + {L("設備描述", "Equipment description")} - 設備編號 + {L("設備編號", "Equipment no.")} @@ -458,7 +469,7 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe {selectedEquipmentDetails.length === 0 ? ( - 沒有選擇的設備 + {L("沒有選擇的設備", "No equipment selected")} ) : ( @@ -495,14 +506,14 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe )} /> { @@ -519,7 +530,7 @@ const QrCodeHandleEquipmentSearch: React.FC = ({ equipmentDetails, printe disabled={checkboxIds.length === 0 || filteredPrinters.length === 0 || !selectedPrinter} color="primary" > - 列印 + {L("列印", "Print")} diff --git a/src/components/qrCodeHandles/qrCodeHandleSearch.tsx b/src/components/qrCodeHandles/qrCodeHandleSearch.tsx index c5dbaf95..d949b630 100644 --- a/src/components/qrCodeHandles/qrCodeHandleSearch.tsx +++ b/src/components/qrCodeHandles/qrCodeHandleSearch.tsx @@ -3,6 +3,7 @@ import SearchBox, { Criterion } from "../SearchBox"; import { useCallback, useMemo, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import SearchResults, { Column } from "../SearchResults"; import DeleteIcon from "@mui/icons-material/Delete"; import { useRouter } from "next/navigation"; @@ -47,6 +48,7 @@ type SearchParamNames = keyof SearchQuery; const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { const { t } = useTranslation("user"); + const L = useUiText(); const [filteredUser, setFilteredUser] = useState(users); const router = useRouter(); const { setIsUploading } = useUploadContext(); @@ -84,7 +86,7 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { const searchCriteria: Criterion[] = useMemo( () => [ { - label: "用戶/姓名", + label: L("用戶/姓名", "User / name"), paramName: "username", type: "text", }, @@ -94,7 +96,7 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { type: "text", }, ], - [t], + [t, L], ); const onDeleteClick = useCallback((user: UserResult) => { @@ -162,13 +164,13 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { const response = await exportUserQrCode(userIds); downloadFile(response.blobValue, response.filename); setSelectedUsersModalOpen(false); - successDialog("二維碼已下載", t); + successDialog(L("二維碼已下載", "QR code downloaded"), t); } catch (error) { console.error("Error exporting QR code:", error); } finally { setIsUploading(false); } - }, [setIsUploading, t]); + }, [L, setIsUploading, t]); const handlePrint = useCallback(async () => { if (checkboxIds.length === 0 || !selectedPrinter) { @@ -182,13 +184,13 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { printQty, }); setSelectedUsersModalOpen(false); - successDialog("二維碼已列印", t); + successDialog(L("二維碼已列印", "QR code printed"), t); } catch (error) { console.error("Error printing QR code:", error); } finally { setIsUploading(false); } - }, [checkboxIds, printQty, selectedPrinter, setIsUploading, t]); + }, [L, checkboxIds, printQty, selectedPrinter, setIsUploading, t]); const handleViewSelectedQrCodes = useCallback(() => { if (checkboxIds.length === 0) { @@ -307,7 +309,10 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { onClick={() => handleSelectAll(!selectAll)} startIcon={} > - 選擇全部用戶 ({checkboxIds.length} / {filteredUser.length}) + {L( + `選擇全部用戶 (${checkboxIds.length} / ${filteredUser.length})`, + `Select all users (${checkboxIds.length} / ${filteredUser.length})`, + )} @@ -350,7 +358,10 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { }} > - 已選擇用戶 ({selectedUsers.length}) + {L( + `已選擇用戶 (${selectedUsers.length})`, + `Selected users (${selectedUsers.length})`, + )} @@ -380,7 +391,7 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { {selectedUsers.length === 0 ? ( - 沒有選擇的用戶 + {L("沒有選擇的用戶", "No users selected")} ) : ( @@ -416,14 +427,14 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { )} /> { @@ -440,7 +451,7 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { disabled={checkboxIds.length === 0 || filteredPrinters.length === 0 || !selectedPrinter} color="primary" > - 列印 + {L("列印", "Print")} diff --git a/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx b/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx index 19d59057..97847a58 100644 --- a/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx +++ b/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { useUiText } from "@/i18n/useUiText"; import SearchResults, { Column } from "../SearchResults"; import { successDialog } from "../Swal/CustomAlerts"; import useUploadContext from "../UploadProvider/useUploadContext"; @@ -44,6 +45,7 @@ interface Props { const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo }) => { const { t } = useTranslation(["warehouse", "common"]); + const L = useUiText(); const [filteredWarehouses, setFilteredWarehouses] = useState(warehouses); const { setIsUploading } = useUploadContext(); const [pagingController, setPagingController] = useState({ @@ -245,13 +247,13 @@ const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo const response = await exportWarehouseQrCode(warehouseIds); downloadFile(response.blobValue, response.filename); setSelectedWarehousesModalOpen(false); - successDialog("二維碼已下載", t); + successDialog(L("二維碼已下載", "QR code downloaded"), t); } catch (error) { console.error("Error exporting QR code:", error); } finally { setIsUploading(false); } - }, [setIsUploading, t]); + }, [L, setIsUploading, t]); const handlePrint = useCallback(async () => { if (checkboxIds.length === 0 || !selectedPrinter) { @@ -265,13 +267,13 @@ const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo printQty, }); setSelectedWarehousesModalOpen(false); - successDialog("二維碼已列印", t); + successDialog(L("二維碼已列印", "QR code printed"), t); } catch (error) { console.error("Error printing QR code:", error); } finally { setIsUploading(false); } - }, [checkboxIds, printQty, selectedPrinter, setIsUploading, t]); + }, [L, checkboxIds, printQty, selectedPrinter, setIsUploading, t]); const handleViewSelectedQrCodes = useCallback(() => { if (checkboxIds.length === 0) { @@ -438,7 +440,10 @@ const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo onClick={() => handleSelectAll(!selectAll)} startIcon={} > - 選擇全部倉庫 ({checkboxIds.length} / {filteredWarehouses.length}) + {L( + `選擇全部倉庫 (${checkboxIds.length} / ${filteredWarehouses.length})`, + `Select all warehouses (${checkboxIds.length} / ${filteredWarehouses.length})`, + )} @@ -481,7 +489,10 @@ const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo }} > - 已選擇倉庫 ({selectedWarehouses.length}) + {L( + `已選擇倉庫 (${selectedWarehouses.length})`, + `Selected warehouses (${selectedWarehouses.length})`, + )} @@ -520,7 +531,7 @@ const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo {selectedWarehouses.length === 0 ? ( - 沒有選擇的倉庫 + {L("沒有選擇的倉庫", "No warehouses selected")} ) : ( @@ -560,14 +571,14 @@ const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo )} /> { @@ -584,7 +595,7 @@ const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo disabled={checkboxIds.length === 0 || filteredPrinters.length === 0 || !selectedPrinter} color="primary" > - 列印 + {L("列印", "Print")} diff --git a/src/config/demoFloors.ts b/src/config/demoFloors.ts index 13dce59b..c9411849 100644 --- a/src/config/demoFloors.ts +++ b/src/config/demoFloors.ts @@ -12,8 +12,8 @@ export const LOC_HOLD = "2F-W202-01-00"; export const LOC_WIP = "2F-W200-#A-00"; export const LOC_PREFIX = "LOC-"; -export const LABEL_A = "Label機 2F A+B"; -export const LABEL_B = "Label機 4F 乾貨 C, D"; +export const LABEL_A = "Label printer 2F A+B"; +export const LABEL_B = "Label printer 4F dry goods C, D"; export const SETTING_SUPPLIERS_A = "DO.floor.suppliers.2F"; export const SETTING_SUPPLIERS_B = "DO.floor.suppliers.4F"; diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 795915fc..abb2ad5a 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -3,8 +3,8 @@ "Add Document": "Add Document", "All": "All", "Allergic Substances": "Allergens", - "An error has occurred. Please try again later.": "An error has occurred. Please try again later.", - "Are you sure you want to delete this item?": "Are you sure you want to delete this item?", + "An error has occurred. Please try again later.": "Something went wrong. Try again later.", + "Are you sure you want to delete this item?": "Delete this item?", "Back": "Back", "Basic Info": "Basic Info", "Bom Required Qty": "BOM Required Qty", @@ -17,28 +17,28 @@ "Coming soon": "Coming soon", "Complexity": "Complexity", "Confirm": "Confirm", - "Confirm Delete": "Confirm Delete", + "Confirm Delete": "Confirm delete", "Cost (HKD)": "Cost (HKD)", "Current total": "Current total", - "Day Before Yesterday": "Day Before Yesterday", + "Day Before Yesterday": "Day before yesterday", "Delete": "Delete", - "Delete Failed": "Delete Failed", - "Do you want to delete?": "Do you want to delete?", + "Delete Failed": "Could not delete", + "Do you want to delete?": "Delete this?", "Density": "Density", - "Depth": "Color depth (dark 1 / light 5)", + "Depth": "Color depth (1 dark – 5 light)", "Description": "Description", "Details": "Details", "Duration (Minutes)": "Duration (Minutes)", "Edit": "Edit", - "Enter any additional observations or notes...": "Enter any additional observations or notes...", - "Enter or select remark": "Enter or select remark", - "Error saving data": "Error saving data", - "Failed to fetch data": "Failed to fetch data", + "Enter any additional observations or notes...": "Enter any extra notes", + "Enter or select remark": "Enter or select a remark", + "Error saving data": "Could not save", + "Failed to fetch data": "Could not load the data", "Filter": "Filter", - "Finished Good Detail": "Finished Good Detail", - "Finished Good Management": "Finished Good Management", - "Finished Good Order": "Finished Good Order", - "Float": "Float", + "Finished Good Detail": "Finished goods detail", + "Finished Good Management": "Finished goods management", + "Finished Good Order": "Finished goods outbound", + "Float": "Float / sink", "General Data": "General Data", "Grade {{grade}}": "Grade {{grade}}", "Invoice": "Invoice", @@ -46,7 +46,7 @@ "IP": "IP", "Item Code": "Item Code", "Item Name": "Item Name", - "Loading": "Loading...", + "Loading": "Loading…", "Loading order summary": "Loading order summary", "Location": "Location", "MA": "Material", @@ -58,16 +58,16 @@ "NM": "Miscellaneous and non-consumables", "No": "No", "No Lot": "No Lot", - "No data available": "No data available", + "No data available": "No data", "No options": "No options", "Order": "Order", "Pending": "Pending", "Port": "Port", - "Please Select BOM": "Please select BOM", - "Please try again later.": "Please try again later.", + "Please Select BOM": "Select a BOM", + "Please try again later.": "Try again later.", "Project Code": "Project Code", "Project Code and Name": "Project Code and Name", - "QC Template not found": "QC template not found", + "QC Template not found": "No QC template was found", "Qty": "Qty", "RM": "Raw material", "Range": "Range", @@ -81,11 +81,11 @@ "Sales Qty": "Sales Qty", "Sales UOM": "Sales UOM", "Save": "Save", - "Saving": "Saving", + "Saving": "Saving…", "Search": "Search", "Search Criteria": "Search Criteria", "Select Date": "Select Date", - "Session expired or unauthorized.": "Session expired or unauthorized.", + "Session expired or unauthorized.": "Your session expired, or you do not have access.", "Sign out": "Sign out", "Language": "Language", "Status": "Status", @@ -95,16 +95,16 @@ "Time Sequence": "Time Sequence", "Type": "Type", "Today": "Today", - "Total weighting must equal 1": "Total weighting must equal 1", - "Unauthorized: Please log in again": "Unauthorized: Please log in again", + "Total weighting must equal 1": "The weights must add up to 1", + "Unauthorized: Please log in again": "Sign in again to continue.", "Uom": "UOM", - "Update Failed": "Update Failed", - "Update Success": "Update Success", - "Weighting must be a number": "Weighting must be a number", + "Update Failed": "Could not update", + "Update Success": "Updated", + "Weighting must be a number": "Weight must be a number", "Yes": "Yes", "Yesterday": "Yesterday", "all": "All", - "bomWeighting": "BOM Weighting Score", + "bomWeighting": "BOM weighting score", "cmb": "Consumable", "collapsible table": "Collapsible table", "consumable": "Consumable", @@ -124,7 +124,7 @@ "revert": "Revert", "settings": "Settings", "stockRecord": "Stock record", - "stocktakemanagement": "Stock take management", + "stocktakemanagement": "Stock take", "testing sections tabs": "Testing section tabs", "warehouse": "Warehouse", "材料": "Material" diff --git a/src/i18n/en/dashboard.json b/src/i18n/en/dashboard.json index fc5c3519..2bb5a331 100644 --- a/src/i18n/en/dashboard.json +++ b/src/i18n/en/dashboard.json @@ -1,75 +1,75 @@ { - "Add some entries!": "Add some entries!", + "Add some entries!": "Nothing here yet.", "All": "All", - "All Stores": "All Stores", - "Allow to select Date to view history.": "Allow to select Date to view history.", - "Application completion": "Application completion", - "Auto-refresh every 1 minute": "Auto-refresh every 1 minute", - "Auto-refresh every 10 minutes": "Auto-refresh every 10 minutes", - "Auto-refresh every 15 minutes": "Auto-refresh every 15 minutes", - "Auto-refresh every 5 minutes": "Auto-refresh every 5 minutes", + "All Stores": "All floors", + "Allow to select Date to view history.": "Select a date to view history.", + "Application completion": "Pick request progress", + "Auto-refresh every 1 minute": "Refreshes every minute", + "Auto-refresh every 10 minutes": "Refreshes every 10 minutes", + "Auto-refresh every 15 minutes": "Refreshes every 15 minutes", + "Auto-refresh every 5 minutes": "Refreshes every 5 minutes", "Based on Expected Delivery Date": "Based on Expected Delivery Date", - "Cold storage": "Cold storage", + "Cold storage": "Cold store", "Column 1": "Column 1", "Column 2": "Column 2", "Column 3": "Column 3", "Completed QC Total": "Completed QC Total", "Consumable": "Consumable", - "Count any item with IQC defect in any IQC criteria": "Count any item with IQC defect in any IQC criteria", + "Count any item with IQC defect in any IQC criteria": "Counts any item that failed any incoming QC check.", "DN Date": "DN Date", "Dashboard": "Dashboard", "dashboard": "Dashboard", "dashboard tabs": "Dashboard tabs", "Date": "Date", - "Day After Tomorrow": "Day After Tomorrow", + "Day After Tomorrow": "Day after tomorrow", "Escalation Level": "Escalation Level", "Escalation List": "Escalation List", "Exit Screen": "Exit Screen", - "Expected No. of Delivery": "Expected No. of Delivery", - "Extracted order": "Extracted order", + "Expected No. of Delivery": "Expected deliveries", + "Extracted order": "Pick orders created", "Filter": "Filter", - "First Ticket Start": "First Ticket Start", + "First Ticket Start": "First ticket started", "Goods Receipt Status": "Goods Receipt Status", - "Goods Receipt Status New": "Goods Receipt Status", + "Goods Receipt Status New": "Purchase order receipt status", "Humidity": "Humidity", "Humidity status": "Humidity status", "Item Name": "Item Name", "Last 24 hours": "Last 24 hours", "Last 6 hours": "Last 6 hours", - "Last Ticket End": "Last Ticket End", + "Last Ticket End": "Last ticket finished", "Last updated": "Last updated", "My Escalation List": "My Escalation List", "No": "No", - "No data available": "No data available", - "No truck schedules available": "No truck schedules available", + "No data available": "No data", + "No truck schedules available": "No truck schedule", "Overview": "Overview", - "No truck schedules available for today": "No truck schedules available for today", - "No. of Items Completed Put Away at Store": "No. of Items Completed Put Away at Store", - "No. of Items Inspected": "No. of Items Inspected", - "No. of Items with IQC Issue": "No. of Items with IQC Issue", - "No. of Orders Received at Dock": "No. of Orders Received at Dock", - "No. of Shops": "No. of Shops", - "Normal temperature storage": "Normal temperature storage", + "No truck schedules available for today": "No truck schedule for today", + "No. of Items Completed Put Away at Store": "Items put away", + "No. of Items Inspected": "Items inspected", + "No. of Items with IQC Issue": "Items with an incoming QC issue", + "No. of Orders Received at Dock": "Orders received at the dock", + "No. of Shops": "Shops (tickets)", + "Normal temperature storage": "Ambient store", "Now": "Now", "Order completion": "Order completion", "Order status": "Order status", - "Pending application": "Pending application", - "Pending order": "Pending order", - "Pending storage": "Pending storage", - "Pick Time (min)": "Pick Time (min)", - "Po Code": "Po Code", - "Po Code/Jo Code": "Po Code/Jo Code", - "Processed application": "Processed application", + "Pending application": "Pick requests waiting", + "Pending order": "Pick orders waiting", + "Pending storage": "Waiting to be put away", + "Pick Time (min)": "Pick time (min)", + "Po Code": "PO code", + "Po Code/Jo Code": "PO code / JO code", + "Processed application": "Pick requests processed", "Progress chart": "Progress chart", "Purchase Order Code": "Purchase Order Code", - "Purchase UoM": "Purchase UoM", + "Purchase UoM": "Purchase UOM", "QC Completed Count": "QC Completed Count", "QC Fail Count": "QC Fail Count", "QC Fail-Total Count": "QC Fail-Total Count", "Reason": "Reason", "Received Qty": "Received Qty", "Responsible Escalation List": "Responsible Escalation List", - "Responsible for handling colleagues": "Responsible for handling colleagues", + "Responsible for handling colleagues": "Colleague responsible", "Restore Screen": "Restore Screen", "Rows per page": "Rows per page", "Screen cleared": "Screen cleared", @@ -80,58 +80,58 @@ "Show Supplier Name": "Show Supplier Name", "Statistics": "Statistics", "Status": "Status", - "Store ID": "Store ID", - "Store Management": "Store Management", + "Store ID": "Floor", + "Store Management": "Materials", "Supplier": "Supplier", "Supplier Code": "Supplier Code", "Supplier Name": "Supplier Name", "Temperature": "Temperature", "Temperature status": "Temperature status", - "Tickets Completed": "Tickets Completed", - "Tickets Released": "Tickets Released", + "Tickets Completed": "Tickets completed", + "Tickets Released": "Tickets released", "Time": "Time", "Time Remaining": "Time Remaining", - "Two column navigable table": "Two column navigable table", + "Two column navigable table": "Two-column table", "Today": "Today", "Tomorrow": "Tomorrow", "Total Items": "Total Items", - "Total storage": "Total storage", + "Total storage": "Put away", "Truck Schedule": "Truck Schedule", "Truck Schedule Dashboard": "Truck Schedule Dashboard", - "Upon any IQC decision received": "Upon any IQC decision received", - "Upon completion of put away for an material in order. Count no. of items being put away": "Upon completion of put away for an material in order. Count no. of items being put away", - "Upon entry of DN and Lot No. for all items of the order": "Upon entry of DN and Lot No. for all items of the order", - "Usage statistics": "Usage statistics", - "Usage stats description": "Report management and truck routing summary: page views, downloads, and direct prints.", + "Upon any IQC decision received": "When any incoming QC decision is recorded.", + "Upon completion of put away for an material in order. Count no. of items being put away": "Counts items once put-away is finished for a material on the order.", + "Upon entry of DN and Lot No. for all items of the order": "When a delivery note and lot number have been entered for every item on the order.", + "Usage statistics": "Report and truck-routing usage", + "Usage stats description": "Page views, downloads, and direct prints for reports and the truck routing summary.", "Usage stats downloads": "Downloads", "Usage stats end date": "End date", - "Usage stats invalid date range": "Start date must be earlier than or equal to end date.", + "Usage stats invalid date range": "The start date must be on or before the end date.", "Usage stats load error": "Could not load usage statistics.", "Usage stats page views": "Page views", "Usage stats prints": "Direct prints", "Usage stats refresh": "Refresh", - "Usage stats report section": "Usage stats report section", + "Usage stats report section": "Report management", "Usage stats search": "Search", "Usage stats start date": "Start date", - "Usage stats truck routing section": "Usage stats truck routing section", + "Usage stats truck routing section": "Truck routing summary", "Usage stats user": "User", "Warehouse status": "Warehouse status", "Warehouse temperature record": "Warehouse temperature record", "Warehouse type": "Warehouse type", - "accepted": "accepted", - "escalateFrom": "escalateFrom", - "escalated": "escalated", - "escalated date": "escalated date", - "escalated datetime": "escalated datetime", - "escalationStatus": "escalationStatus", - "inspected material": "inspected material", - "pending": "pending", - "pending inspection material": "pending inspection material", - "receiving": "receiving", - "rejected": "rejected", - "show completed logs": "show completed logs", - "stock in escalation list": "stock in escalation list", - "total": "total", - "total material": "total material", - "x/y orders received": "x/y orders received" + "accepted": "Received", + "escalateFrom": "Escalated by", + "escalated": "Escalated", + "escalated date": "Escalation date", + "escalated datetime": "Escalation time", + "escalationStatus": "Escalation status", + "inspected material": "Inspected", + "pending": "Pending", + "pending inspection material": "Waiting for inspection", + "receiving": "Receiving", + "rejected": "Rejected", + "show completed logs": "Show completed escalations", + "stock in escalation list": "Receipt escalations", + "total": "Still open", + "total material": "Total items", + "x/y orders received": "x/y orders processed" } diff --git a/src/i18n/en/do.json b/src/i18n/en/do.json index ecae8209..906107e1 100644 --- a/src/i18n/en/do.json +++ b/src/i18n/en/do.json @@ -1,13 +1,13 @@ { "Action": "Action", - "An error has occurred. Please try again later.": "An error has occurred. Please try again later.", + "An error has occurred. Please try again later.": "Something went wrong. Try again later.", "Assign 2/F": "Assign 2/F", "Assign 4/F": "Assign 4/F", "Available Orders": "Available Orders", "Available Trucks": "Available Trucks", "Back": "Back", "Batch Release": "Batch Release", - "Batch release completed successfully.": "Batch release completed successfully.", + "Batch release completed successfully.": "Batch release completed.", "Cancel": "Cancel", "Code": "Code", "Completed": "Completed", @@ -15,7 +15,7 @@ "Confirm Assignment": "Confirm Assignment", "Create": "Create", "DO Workbench": "DO Workbench", - "DO released successfully! Pick orders created.": "DO released successfully! Pick orders created.", + "DO released successfully! Pick orders created.": "Delivery order released. Pick orders were created.", "Delete": "Delete", "Delivery Order": "Delivery Order", "Delivery Order Code": "Delivery Order Code", @@ -23,26 +23,26 @@ "Delivery Order No.": "Delivery Order No.", "Delivery Order Status": "Delivery Order Status", "Details": "Details", - "Do Workbench": "Do Workbench", + "Do Workbench": "DO Workbench", "Edit": "Edit", "Edit Delivery Order Detail": "Edit Delivery Order Detail", "Estimated Arrival": "Estimated Arrival", "Estimated Arrival Date": "Estimated Arrival Date", "Estimated Arrival From": "Estimated Arrival From", "Estimated Arrival To": "Estimated Arrival To", - "Etra": "Etra", - "Merge extra orders into lane batch ticket": "Merge into lane merge ticket (isExtrabatch, TI-M- prefix)", + "Etra": "Extra", + "Merge extra orders into lane batch ticket": "Merge extra orders on the same lane into one ticket (TI-M- prefix).", "Confirm merge release": "Confirm merge release", "Confirm release without merge": "Confirm release without merge", "Expiry Date": "Expiry Date", - "Failed to assign pick orders. Please try again later.": "Failed to assign pick orders. Please try again later.", - "Failed to release pick orders. Please try again later.": "Failed to release pick orders. Please try again later.", - "Fetching all matching records...": "Fetching all matching records...", + "Failed to assign pick orders. Please try again later.": "Could not assign the pick orders. Try again later.", + "Failed to release pick orders. Please try again later.": "Could not release the pick orders. Try again later.", + "Fetching all matching records...": "Loading matching records…", "Floor": "Floor", "Item Name": "Item Name", "Item No.": "Item No.", - "Just Complete": "Just Complete", - "Just Completed": "Just Completed", + "Just Complete": "Complete", + "Just Completed": "Completed", "Lane Code": "Lane Code", "Loading...": "Loading...", "Location": "Location", @@ -62,19 +62,19 @@ "Pick Order Assignment": "Pick Order Assignment", "Please wait": "Please wait", "Price": "Price", - "Problem DO(s): ": "Problem DO(s): ", + "Problem DO(s): ": "Delivery orders with a problem: ", "Progress": "Progress", "Quantity": "Quantity", "All": "All", "Add Row": "Add", "Created": "Created", - "DO code suffix must be exactly 4 characters": "DO code suffix must be exactly 4 characters", + "DO code suffix must be exactly 4 characters": "The delivery order suffix must be exactly 4 characters", "DO Code Last 4": "DO No. (last 4)", "Delivery Date": "Delivery Date", - "Enter last 4 characters of DO code": "Enter last 4 characters of DO code", + "Enter last 4 characters of DO code": "Enter the last 4 characters of the delivery order number", "Shop code, or first characters of shop name": "Shop code (partial match), or first characters of shop name", "Multiple source DOs matched": "Multiple original DOs matched", - "Please verify DO code suffix, delivery date and shop.": "Please verify DO code suffix, delivery date and shop.", + "Please verify DO code suffix, delivery date and shop.": "Check the delivery order suffix, delivery date, and shop.", "Shop code or name is required": "Shop code or name is required", "Draft List": "Draft List", "Replenishment preview hint": "Add items from different original DOs, then batch submit from here.", @@ -97,7 +97,7 @@ "Please lookup source DO first": "Please match original DO first", "Picker Name": "Picker Name", "Please select an item": "Please select an item", - "Records saved locally for preview. Backend integration pending.": "Records saved locally for preview. Backend integration pending.", + "Records saved locally for preview. Backend integration pending.": "Saved in this browser for preview. It is not stored on the server yet.", "Replenishment Code": "Replenishment No.", "Ref Code": "Ref Code", "Replenish Qty": "Replenish Qty", @@ -155,24 +155,24 @@ "Stock Status": "Stock Status", "Delivery": "Delivery", "Store": "Store", - "Submit Miss Item": "Submit Miss Item", + "Submit Miss Item": "Submit missing items", "Submit Qty": "Submit Qty", "Submit Quantity": "Submit Quantity", "Supplier Code": "Supplier Code", "Supplier Name": "Supplier Name", "Truck Availability Warning": "Truck Availability Warning", - "Truck Lance Code": "Truck Lance Code", + "Truck Lance Code": "Truck lane code", "Truck Lane": "Truck Lane", "Truck X": "Truck X", - "Truck lane search requires date message": "Truck lane search requires date message", - "Truck lane search requires date title": "Truck lane search requires date title", + "Truck lane search requires date message": "When a truck lane number is entered, select an estimated delivery date before searching.", + "Truck lane search requires date title": "Estimated delivery date required", "User session not found": "User session not found. Please login again.", "Error": "Error", "OK": "OK", - "Warning: Some delivery orders do not have matching trucks for the target date.": "Warning: Some delivery orders do not have matching trucks for the target date.", + "Warning: Some delivery orders do not have matching trucks for the target date.": "Some delivery orders have no truck for the target date.", "Workbench Batch Release": "Workbench Batch Release", - "code": "code", - "do workbench": "do workbench", - "row selected": "row selected", + "code": "Code", + "do workbench": "DO Workbench", + "row selected": "Row selected", "uom": "Unit" } diff --git a/src/i18n/en/doWorkbench.json b/src/i18n/en/doWorkbench.json index a74b25be..d558cbe5 100644 --- a/src/i18n/en/doWorkbench.json +++ b/src/i18n/en/doWorkbench.json @@ -21,7 +21,7 @@ "Location": "Location", "Lot No": "Lot No", "Lot Required Pick Qty": "Lot Required Pick Qty", - "No data available": "No data available", + "No data available": "No data", "Now": "Now", "Pick Order": "Pick Order", "Pick Order Code": "Pick Order Code", @@ -35,21 +35,21 @@ "Start QR Scan": "Start QR Scan", "Status": "Status", "Stop QR Scan": "Stop QR Scan", - "Store ID": "Store ID", + "Store ID": "Floor", "Submitting...": "Submitting...", "Target Date": "Target Date", - "This lot is rejected, please scan another lot.": "This lot is rejected, please scan another lot.", + "This lot is rejected, please scan another lot.": "This lot was rejected. Scan another lot.", "Today": "Today", "Truck Information": "Truck Information", "View Details": "View Details", "DO Workbench Search": "DO Workbench Search", - "completed": "completed", - "items": "items", - "pending": "pending", - "Pick Order Detail": "Pick Detail", - "Etra Pick Order Detail": "Etra", - "Finished Good Record": "FG Record", - "Finished Good Record (All)": "FG Record (All)", + "completed": "Completed", + "items": "Items", + "pending": "Pending", + "Pick Order Detail": "Pick detail", + "Etra Pick Order Detail": "Extra pick order", + "Finished Good Record": "Finished goods record", + "Finished Good Record (All)": "Finished goods record (all)", "Ticket Release Table": "Ticket Release", "FG Carton Qty": "Carton Qty", "成品出倉出箱數量": "Carton Qty", @@ -77,15 +77,15 @@ "Date": "Date", "Download Excel": "Download Excel", "Download this view Excel": "Download Excel (Selected)", - "Download period Excel": "Download Excel (Complete)", - "Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.", - "Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.", + "Download period Excel": "Download Excel (full period)", + "Download this view Excel hint": "Excel for the current floor, lane, shop, and date, plus the chart breakdown.", + "Download period Excel hint": "Excel for the last 7 days, this month, and this year. Not limited to the current filters.", "Filtered": "Filtered", "Breakdown": "Breakdown", "All floors": "All floors", - "FG carton qty filtered title": "FG carton qty (filtered) - {{floor}} - {{lane}} - {{shop}} - {{date}}", + "FG carton qty filtered title": "Carton qty (filtered) – {{floor}} – {{lane}} – {{shop}} – {{date}}", "Generating...": "Generating...", "Download report (PDF)": "Download report (PDF)", - "Unpicked orders confirm download": "This lane still has {{count}} unpicked order(s).\nPrint / download the truck routing summary anyway?", - "Failed to download Workbench truck routing summary. Please try again later.": "Failed to download Workbench truck routing summary. Please try again later." + "Unpicked orders confirm download": "This lane still has {{count}} unpicked order(s).\nDownload the truck routing summary anyway?", + "Failed to download Workbench truck routing summary. Please try again later.": "Could not download the truck routing summary. Try again later." } diff --git a/src/i18n/en/finishedgoodmanagement.json b/src/i18n/en/finishedgoodmanagement.json index d978c7a6..bf6a6ad6 100644 --- a/src/i18n/en/finishedgoodmanagement.json +++ b/src/i18n/en/finishedgoodmanagement.json @@ -10,7 +10,7 @@ "Move Top": "Move to Top", "Move Bottom": "Move to Bottom", "Add Item": "Add Item", - "Saving": "Saving", + "Saving": "Saving…", "Save": "Save", "Refresh": "Reload", "Unsaved changes": "Unsaved changes", @@ -23,7 +23,7 @@ "Stock Out Location": "Stock Out Location", "Actions": "Actions", "Loading": "Loading...", - "No data available": "No data available", + "No data available": "No data", "Insert": "Insert", "Search": "Search", "Insert at": "Insert at", @@ -31,7 +31,7 @@ "Order number": "Order number", "In front of": "In front of", "Behind": "Behind", - "Only show FG items without order": "Enter a keyword to search (only items without sequence)", + "Only show FG items without order": "Enter a keyword. Only items with no sequence are searched.", "Cancel": "Cancel", "Confirm": "Confirm", "Move to order": "Move to order", diff --git a/src/i18n/en/home.json b/src/i18n/en/home.json index 5f5738dc..e1ac8197 100644 --- a/src/i18n/en/home.json +++ b/src/i18n/en/home.json @@ -1,4 +1,4 @@ { - "Add some entries!": "Add some entries!", + "Add some entries!": "Nothing here yet.", "Demand Qty": "Demand Qty" } diff --git a/src/i18n/en/importBom.json b/src/i18n/en/importBom.json index 17fcd272..b951bd4f 100644 --- a/src/i18n/en/importBom.json +++ b/src/i18n/en/importBom.json @@ -1,12 +1,12 @@ { - "Code": "Code", - "Name": "Name", + "Code": "BOM Code", + "Name": "BOM Name", "Create Material": "Create Material", "Update Equipment Maintenance and Repair": "Update Equipment Maintenance and Repair", - "Correct BOM List (Can Import)": "Correct BOM List (Can Import)", - "Issue BOM List": "Issue BOM List", - "Loading BOM Detail...": "Loading BOM Detail...", - "Bom Material": "Bom Material", + "Correct BOM List (Can Import)": "BOMs that can be imported", + "Issue BOM List": "BOMs with problems", + "Loading BOM Detail...": "Loading BOM details…", + "Bom Material": "Material list", "Is Drink": "Is Drink", "Drink": "Drink", "Powder_Mixture": "Powder Mixture", @@ -32,7 +32,7 @@ "No scale data": "No scale data", "Back to reselect files": "Back to reselect files", "Upload check summary": "Uploaded {{uploaded}} file(s). Check result: {{total}} total — {{correct}} passed, {{failed}} failed", - "Upload count mismatch hint": "Upload count does not match check count; duplicate file names may have been renamed with _2, _3, etc.", + "Upload count mismatch hint": "The upload count does not match the check count. Duplicate file names may have been renamed with _2, _3, and so on.", "Search file name": "Search file name", "Search code or name": "Search code or name", "Format check issues": "Format check issues", @@ -56,7 +56,7 @@ "Bom Kind Mode": "Kind", "Bom Kind FG": "FG", "Bom Kind WIP": "WIP", - "Bom Kind Both": "both", + "Bom Kind Both": "Both", "File Name": "File Name", "Base Score": "Base Score", "BOM Status": "BOM Status", @@ -83,15 +83,12 @@ "Recipe UOM": "Recipe UOM", "Edit Materials": "Edit materials", "Save New Version": "Save as new version", - "Material edit creates new version hint": "Editing material qty/UOM creates a new version and activates it. Status-only changes do not create a new version.", + "Material edit creates new version hint": "Changing a material quantity or unit creates and activates a new version. Changing status only does not.", "Content": "Content", "Saving...": "Saving...", - "Code": "BOM Code", - "Name": "BOM Name", "Output Quantity": "Output Quantity", "Output Quantity UOM": "Output Quantity UOM", "Type": "Type", - "Loading BOM Detail...": "Loading BOM Detail...", "Basic Info": "Basic Info", "Edit Basic Info": "Edit basic info", "Edit": "Edit", @@ -101,7 +98,7 @@ "Allergic Substances": "Allergic Substances", "Depth": "Color depth", "Depth hint": "Dark 1 → Light 5", - "Float": "Float", + "Float": "Float / sink", "Density": "Density", "Time Sequence": "Time Sequence", "Complexity": "Complexity", @@ -121,7 +118,7 @@ "Putaway Area": "Area", "Putaway Slot": "Slot", "Item Location Code": "Item location code", - "Putaway location item hint": "QC auto putaway still uses item master LocationCode; this field is stored per BOM revision.", + "Putaway location item hint": "Automatic QC put-away still uses the item's location code. This field is stored on the BOM revision.", "Putaway edit creates new version hint": "Changing putaway location creates a new version and updates the item master location.", "Process & Equipment": "Process & Equipment", "Process Code": "Process Code", diff --git a/src/i18n/en/inventory.json b/src/i18n/en/inventory.json index d27a641a..9911e719 100644 --- a/src/i18n/en/inventory.json +++ b/src/i18n/en/inventory.json @@ -27,7 +27,7 @@ "Confirm remove": "Confirm remove", "Current Stock": "Current Stock", "Difference": "Difference", - "Download QR Code": "Download QR Code", + "Download QR Code": "Download QR code", "Edit mode": "Edit mode", "Enter item code or name to search": "Enter item code or name to search", "Expiry Date": "Expiry Date", @@ -82,7 +82,7 @@ "Original Qty": "Original Qty", "Please scan...": "Please scan...", "Print": "Print", - "Print QR Code": "Print QR Code", + "Print QR Code": "Print QR code", "Print Qty": "Print Qty", "Print failed": "Print failed", "Print sent": "Print sent", @@ -103,7 +103,7 @@ "Start Location": "Start Location", "Stock Adjustment": "Stock Adjustment", "Stock Transfer": "Stock Transfer", - "Stock UoM": "Stock UoM", + "Stock UoM": "Stock UOM", "Stock transfer created new lot": "Stock transfer completed (created new lot line).", "Stock transfer merged ambiguous": "Merged into the earliest available batch (multiple available lines for the same lot).", "Stock transfer merged existing lot": "Stock transfer completed (merged into existing lot).", @@ -112,19 +112,19 @@ "Submit": "Submit", "Target Location": "Target Location", "Type": "Type", - "UoM": "UoM", + "UoM": "UOM", "WIP": "Work in progress", "Warehouse": "Warehouse", "cmb": "Consumables", "consumable": "Consumable", "consumables": "Consumables", - "dnNo": "dnNo", + "dnNo": "Delivery note no.", "fg": "Finished good", "item": "Item", "mat": "Raw material", "nm": "Miscellaneous / non-consumables", "non-consumables": "Non-consumables", - "productLotNo": "productLotNo", + "productLotNo": "Product lot no.", "sfg": "Semi-finished good", "to": "to", "wip": "Work in progress", diff --git a/src/i18n/en/itemDefaultShelfLife.json b/src/i18n/en/itemDefaultShelfLife.json index 7f90cf3c..1ba8cfc5 100644 --- a/src/i18n/en/itemDefaultShelfLife.json +++ b/src/i18n/en/itemDefaultShelfLife.json @@ -6,7 +6,7 @@ "Edit": "Edit", "Delete": "Delete", "Save": "Save", - "Saving": "Saving", + "Saving": "Saving…", "Cancel": "Cancel", "Saved": "Saved", "Deleted": "Deleted", diff --git a/src/i18n/en/itemTracing.json b/src/i18n/en/itemTracing.json index 5a218103..de215f22 100644 --- a/src/i18n/en/itemTracing.json +++ b/src/i18n/en/itemTracing.json @@ -1,6 +1,6 @@ { "title": "Item Tracing", - "subtitle": "Scan a lot QR code to trace the full lifecycle — stock in/out, QC, pick orders, job orders, stock take, transfers, and BOM links.", + "subtitle": "Scan a lot QR code to see its full history: stock in and out, QC, pick orders, job orders, stock take, transfers, and BOM links.", "scanReady": "Scanner ready — scan lot label QR", "scanning": "Scanning…", "scanAgain": "Scan again", @@ -13,7 +13,7 @@ "noResult": "No trace data. Scan a lot QR or search by item code and lot number.", "notFound": "Lot not found or no permission to view.", "traceError": "Unable to load trace data. Please try again or contact support.", - "scanError": "Invalid QR code. Expected lot label JSON with itemId and stockInLineId.", + "scanError": "This QR code is not a lot label. It needs an item and a stock-in line.", "summary": "Lot summary", "expiryDate": "Expiry", "productionDate": "Production", @@ -85,7 +85,7 @@ "nodeFail": "Pick failure", "nodeDoOut": "Delivery outbound", "nodeReplenishmentCreated": "Replenishment created", - "doOutboundExtra": "Add-on", + "doOutboundExtra": "Extra", "doOutboundReplenish": "Replenishment", "detailDoOutboundKind": "Outbound type", "nodeDoGroup": "Delivery outbound group", @@ -327,7 +327,7 @@ "lastMove": "Last move", "N/A": "N/A", "pendingPick": "Pending pick", - "detailBomReqQty": "BOM req. qty", + "detailBomReqQty": "BOM required qty", "detailStockReqQty": "Stock req. qty", "detailStockAvailable": "Stock available", "detailStockStatus": "Stock status", diff --git a/src/i18n/en/items.json b/src/i18n/en/items.json index 859f7ef7..7d27e9c6 100644 --- a/src/i18n/en/items.json +++ b/src/i18n/en/items.json @@ -19,7 +19,7 @@ "LocationCode": "Location Code", "Missing Data": "Missing Data", "Name": "Name", - "No QC items in this template": "No QC items in this template", + "No QC items in this template": "This template has no QC items", "None": "None", "Product": "Product", "Product / Material": "Product / Material", @@ -46,25 +46,25 @@ "mat": "MAT", "cmb": "CMB", "nm": "NM", - "code": "code", - "countryOfOrigin": "countryOfOrigin", - "description": "description", + "code": "Code", + "countryOfOrigin": "Country of origin", + "description": "Description", "instruction": "instruction", "isBag": "Bag", "isEgg": "Egg", "isFee": "Fee", - "lowerLimit": "lowerLimit", - "maxQty": "maxQty", - "name": "name", - "no rows": "no rows", - "remarks": "remarks", - "shelfLife": "shelfLife", - "upperLimit": "upperLimit", + "lowerLimit": "Lower limit", + "maxQty": "Maximum qty", + "name": "Name", + "no rows": "No data", + "remarks": "Remarks", + "shelfLife": "Shelf life", + "upperLimit": "Upper limit", "Item Code": "Item Code", "Item Name": "Item Name", "Sales Qty": "Sales Qty", "Sales UOM": "Sales UOM", "Stock Qty": "Stock Qty", - "Uom": "Uom", + "Uom": "UOM", "Cost (HKD)": "Cost (HKD)" } diff --git a/src/i18n/en/jo.json b/src/i18n/en/jo.json index 06a212c3..030d5e12 100644 --- a/src/i18n/en/jo.json +++ b/src/i18n/en/jo.json @@ -1,26 +1,26 @@ { - "2/F plastic box carton Qty": "2/F plastic box carton Qty", - "2/F plastic box carton qty count": "2/F plastic box carton qty count", - "3/F plastic box carton Qty": "3/F plastic box carton Qty", - "3/F plastic box carton qty count": "3/F plastic box carton qty count", - "4/F plastic box carton Qty": "4/F plastic box carton Qty", - "4/F plastic box carton qty count": "4/F plastic box carton qty count", + "2/F plastic box carton Qty": "2/F plastic crate qty", + "2/F plastic box carton qty count": "2/F plastic crate qty", + "3/F plastic box carton Qty": "3/F plastic crate qty", + "3/F plastic box carton qty count": "3/F plastic crate qty", + "4/F plastic box carton Qty": "4/F plastic crate qty", + "4/F plastic box carton qty count": "4/F plastic crate qty", "Action": "Action", "Actions": "Actions", "Actual Pick Qty": "Actual Pick Qty", "Add Bag": "Add Bag", "Add Record": "Add Record", "Add Selected Items to Created Items": "Add Selected Items to Created Items", - "Add some entries!": "Add some entries!", + "Add some entries!": "Nothing here yet.", "All": "All", "All Pick Order Lots": "All Pick Order Lots", "All floors": "All floors", - "All pick orders created successfully": "All pick orders created successfully", - "Are you sure you want to delete this procoess?": "Are you sure you want to delete this procoess?", + "All pick orders created successfully": "All pick orders were created.", + "Are you sure you want to delete this procoess?": "Are you sure you want to delete this process?", "Assignment failed: ": "Assignment failed: ", "Assignment successful": "Assignment successful", - "Assume End Time": "Assume End Time", - "At least one issue must be reported": "At least one issue must be reported", + "Assume End Time": "Expected end time", + "At least one issue must be reported": "Report at least one issue", "Auto-refresh every 1 minute": "Auto-refresh every 1 minute", "Auto-refresh every 10 minutes": "Auto-refresh every 10 minutes", "Auto-refresh every 15 minutes": "Auto-refresh every 15 minutes", @@ -32,8 +32,8 @@ "BOM Type": "BOM Type", "Back": "Back", "Back to List": "Back to List", - "Bad Item Qty": "Bad Item Qty", - "Bad Package Qty": "Bad Package Qty", + "Bad Item Qty": "Damaged qty", + "Bad Package Qty": "Damaged packs", "Bag": "Bag", "Bag Code": "Bag Code", "Bag Consumption": "Bag Consumption", @@ -47,18 +47,18 @@ "Base Qty": "Base Qty", "Base UOM": "Base UOM", "Batch Count": "Batch Count", - "BoM Material": "BoM Material", - "Bom": "Bom", - "Bom Req. Qty": "Bom Req. Qty", - "Bom Uom": "Bom Uom", + "BoM Material": "BOM material", + "Bom": "BOM", + "Bom Req. Qty": "BOM required qty", + "Bom Uom": "BOM UOM", "By-product": "By-product", "COMPLETED": "COMPLETED", "Cancel": "Cancel", "Cancel Job Order": "Cancel Job Order", - "Cancel job order confirm message": "Cancel job order confirm message", + "Cancel job order confirm message": "Cancel this job order? It will be hidden from the list.", "Changeover Time": "Changeover Time", - "Changeover Time (mins)": "Changeover Time (mins)", - "Changeover Time (mins)": "Changeover Time (mins)", + "Changeover Time (mins)": "Changeover time (min)", + "Changeover Time (mins)": "Changeover time (min)", "Clean Record": "Clean Record", "Code": "Code", "Code / Lot No": "Code / Lot No", @@ -69,14 +69,14 @@ "Confirm": "Confirm", "Confirm All": "Confirm All", "Confirm Lot Substitution": "Confirm Lot Substitution", - "Confirm cancel job order": "Confirm cancel job order", - "Confirm delete job order": "Confirm delete job order", + "Confirm cancel job order": "Confirm cancellation", + "Confirm delete job order": "Confirm deletion", "Consolidate": "Consolidate", "Consolidated Code": "Consolidated Code", "Consumable": "Consumable", "Consumed Qty": "Consumed Qty", "Continue": "Continue", - "Count of Job Orders": "Count of Job Orders", + "Count of Job Orders": "Job orders", "Create": "Create", "Create Job Order": "Create Job Order", "Create New Group": "Create New Group", @@ -90,8 +90,8 @@ "Default Warehouse": "Default Warehouse", "Defect": "Defect", "Delete Job Order": "Delete Job Order", - "Delete job order confirm message": "Delete job order confirm message", - "Deliver Order": "Deliver Order", + "Delete job order confirm message": "Delete this job order? This cannot be undone.", + "Deliver Order": "Delivery Order", "Delivery Code": "Delivery Code", "Delivery Date": "Delivery Date", "Delivery Note Code": "Delivery Note Code", @@ -99,16 +99,16 @@ "Demand Forecast": "Demand Forecast", "Demand Forecast Setting": "Demand Forecast Setting", "Departure Time": "Departure Time", - "Describe the issue with bad items": "Describe the issue with bad items", + "Describe the issue with bad items": "Describe the damaged items", "Description": "Description", "Detail Scheduling": "Detail Scheduling", "Details": "Details", - "Do you want to start job order": "Do you want to start job order", + "Do you want to start job order": "Start this job order?", "Download Excel": "Download Excel", "Drink": "Drink", - "Duration hours": "Duration hours", - "Duration minutes": "Duration minutes", - "Duration seconds": "Duration seconds", + "Duration hours": "Hours", + "Duration minutes": "Minutes", + "Duration seconds": "Seconds", "Edit": "Edit", "Edit Equipment": "Edit Equipment", "Edit Equipment Type": "Edit Equipment Type", @@ -116,10 +116,10 @@ "Edit Job Order Detail": "Edit Job Order Detail", "End Product": "End Product", "End Qty": "End Qty", - "Enter all floors plastic box carton qty": "Enter all floors plastic box carton qty", - "Enter bad item quantity (required if no missing items)": "Enter bad item quantity (required if no missing items)", - "Enter missing quantity (required if no bad items)": "Enter missing quantity (required if no bad items)", - "Enter plastic box carton qty": "Enter plastic box carton qty", + "Enter all floors plastic box carton qty": "Enter the plastic crate quantity for each floor", + "Enter bad item quantity (required if no missing items)": "Damaged quantity (required if nothing is missing)", + "Enter missing quantity (required if no bad items)": "Missing quantity (required if nothing is damaged)", + "Enter plastic box carton qty": "Enter the plastic crate quantity", "Enter print quantity": "Enter print quantity", "Enter the number of cartons": "Enter the number of cartons", "Enter the number of cartons: ": "Enter the number of cartons: ", @@ -128,7 +128,7 @@ "Equipment Details": "Equipment Details", "Equipment Name and Code": "Equipment Name and Code", "Equipment Type Details": "Equipment Type Details", - "EquipmentType-EquipmentName-Code": "EquipmentType-EquipmentName-Code", + "EquipmentType-EquipmentName-Code": "Equipment type – name – code", "Escalation History": "Escalation History", "Escalation Info": "Escalation Info", "Escalation Result": "Escalation Result", @@ -142,15 +142,15 @@ "FG & Material Demand Forecast Detail": "FG & Material Demand Forecast Detail", "FG / WIP Item": "FG / WIP Item", "FG Production Schedule": "FG Production Schedule", - "Failed to create group": "Failed to create group", - "Failed to load plastic box carton qty dashboard": "Failed to load plastic box carton qty dashboard", - "Failed to submit scan data. Please try again.": "Failed to submit scan data. Please try again.", + "Failed to create group": "Could not create the group", + "Failed to load plastic box carton qty dashboard": "Could not load the plastic crate dashboard. Try again later.", + "Failed to submit scan data. Please try again.": "Could not submit the scan. Try again.", "Finish": "Finish", "Finished Good Order": "Finished Good Order", "Finished Goods Name": "Finished Goods Name", "Finished Job Order Record": "Finished Job Order Record", "Finished lines": "Finished lines", - "First created group": "First created group", + "First created group": "First group created", "Floor": "Floor", "Group": "Group", "Handled By": "Handled By", @@ -160,23 +160,23 @@ "Import Testing": "Import Testing", "In Progress": "In Progress", "In progress": "In progress", - "In_Progress": "In_Progress", + "In_Progress": "In progress", "Index": "Index", - "Input Equipment is not match with process": "Input Equipment is not match with process", - "Insufficient available quantity on lot (may have been picked by another user)": "Insufficient available quantity on lot (may have been picked by another user)", - "Invalid Stock In Line Id": "Invalid Stock In Line Id", + "Input Equipment is not match with process": "The equipment does not match this process.", + "Insufficient available quantity on lot (may have been picked by another user)": "This lot does not have enough quantity left. Someone else may have picked it.", + "Invalid Stock In Line Id": "Invalid stock-in line.", "Invalid date format": "Invalid date format", "Inventory": "Inventory", - "Is Dark": "Is Dark", - "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity", - "Is Dense": "Is Dense", - "Is Float": "Is Float", + "Is Dark": "Dark", + "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "Dark | dense | float/sink | scrap rate | allergens | time sequence | complexity", + "Is Dense": "Dense", + "Is Float": "Float / sink", "Issue": "Issue", "Issue Remark": "Issue Remark", "Item": "Item", "Item Code": "Item Code", "Item Name": "Item Name", - "Item already exists in created items": "Item already exists in created items", + "Item already exists in created items": "That item is already in the list", "Items": "Items", "Jo Pick Order Detail": "Jo Pick Order Detail", "Job Order": "Job Order", @@ -187,29 +187,29 @@ "Job Order No.": "Job Order No.", "Job Order Pick Execution": "Job Order Pick Execution", "Job Order Pick Order Details": "Job Order Pick Order Details", - "Job Order Pickexcution": "Job Order Pickexcution", + "Job Order Pickexcution": "Job Order Picking", "Job Order Qty": "Job Order Qty", "Job Order Type": "Job Order Type", - "Job Order not found or has no items": "Job Order not found or has no items", + "Job Order not found or has no items": "Job order not found, or it has no items.", "Job Process Status Dashboard": "Job Process Status Dashboard", "Drink Production Qty Dashboard": "Drink Production Qty Dashboard", "Job Type": "Job Type", - "Job dashboard PP status: cancelled": "Job dashboard PP status: cancelled", - "Job dashboard PP status: completed": "Job dashboard PP status: completed", - "Job dashboard PP status: in_progress": "Job dashboard PP status: in_progress", - "Job dashboard PP status: pending": "Job dashboard PP status: pending", - "Job dashboard PP status: stopped": "Job dashboard PP status: stopped", - "Job process detail mode label": "Job process detail mode label", - "Job process detail: equipment": "Job process detail: equipment", - "Job process detail: handler": "Job process detail: handler", - "Job process detail: process name": "Job process detail: process name", - "Job process detail: time": "Job process detail: time", - "Just Complete": "Just Complete", - "Just Completed (workbench): requires valid quantity; expired rows must not use this button.": "Just Completed (workbench): requires valid quantity; expired rows must not use this button.", + "Job dashboard PP status: cancelled": "Process cancelled", + "Job dashboard PP status: completed": "Process completed", + "Job dashboard PP status: in_progress": "Process in progress", + "Job dashboard PP status: pending": "Process pending", + "Job dashboard PP status: stopped": "Process stopped", + "Job process detail mode label": "Process cell display", + "Job process detail: equipment": "Equipment", + "Job process detail: handler": "Staff", + "Job process detail: process name": "Process", + "Job process detail: time": "Time", + "Just Complete": "Complete", + "Just Completed (workbench): requires valid quantity; expired rows must not use this button.": "Material match needs a valid quantity. Expired rows cannot use this button.", "Last 7 days": "Last 7 days", "Last updated": "Last updated", "Latest created group": "Latest created group", - "Lines with insufficient stock: ": "Lines with insufficient stock: ", + "Lines with insufficient stock: ": "Items that could not be picked: ", "Lines with sufficient stock: ": "Lines with sufficient stock: ", "Loading...": "Loading...", "Location": "Location", @@ -220,17 +220,17 @@ "Lot No.": "Lot No.", "Lot Number Mismatch": "Lot Number Mismatch", "Lot Required Pick Qty": "Lot Required Pick Qty", - "Lot has been rejected and marked as unavailable.": "Lot has been rejected and marked as unavailable.", - "LotNo": "LotNo", + "Lot has been rejected and marked as unavailable.": "This lot was rejected and marked unavailable.", + "LotNo": "Lot no.", "Mail": "Mail", - "Management Job Order": "Management Job Order", + "Management Job Order": "Job Order Management", "Manual Input": "Manual Input", "Matching Stock": "Matching Stock", "Material": "Material", "Material Code": "Material Code", "Material Pick Status": "Material Pick Status", "Max": "Max", - "Missing item Qty": "Missing item Qty", + "Missing item Qty": "Missing qty", "Missing items": "Missing items", "Multiplier": "Multiplier", "N/A": "N/A", @@ -238,26 +238,26 @@ "Next": "Next", "Next page": "Next page", "No": "No", - "No Group": "No Group", - "No Item": "No Item", - "No Qc": "No Qc", - "No Uom": "No Uom", - "No Warehouse": "No Warehouse", + "No Group": "No group", + "No Item": "No item", + "No Qc": "No QC", + "No Uom": "No UOM", + "No Warehouse": "No warehouse", "No chart data": "No chart data", - "No completed Job Order pick orders with matching found": "No completed Job Order pick orders with matching found", - "No created items": "No created items", - "No data available": "No data available", + "No completed Job Order pick orders with matching found": "No matching records", + "No created items": "No items added yet", + "No data available": "No data", "No data found": "No data found", - "No lot details available": "No lot details available", + "No lot details available": "No lot details", "No results found": "No results found", "No. of Items to be Picked": "No. of Items to be Picked", "No. of Items with Issue During Pick": "No. of Items with Issue During Pick", "None": "None", "Note:": "Note:", "Now": "Now", - "Number must be at least 1": "Number must be at least 1", + "Number must be at least 1": "Enter a number of at least 1", "Number of cartons": "Number of cartons", - "Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.": "Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.", + "Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.": "Open Lot QR Code on a pick line first, then scan {2fic} to substitute the lot manually.", "Operator": "Operator", "Operator KPI": "Operator KPI", "Operator KPI Dashboard": "Operator KPI Dashboard", @@ -267,25 +267,25 @@ "Other": "Other", "Output Qty": "Output Qty", "Output from Process": "Output from Process", - "Over Time": "Over Time", + "Over Time": "Over time", "Overall Time Remaining": "Overall Time Remaining", "Overview": "Overview", "Packaging": "Packaging", - "Partial quantity submitted. Please submit more or complete the order.": "Partial quantity submitted. Please submit more or complete the order.", + "Partial quantity submitted. Please submit more or complete the order.": "Only part of the quantity was submitted. Submit the rest, or complete the order.", "Pass": "Skip", "Just Pass": "Skip", "Auto Pass": "Auto Skipped", - "Passed Step": "Passed Step", + "Passed Step": "Skipped step", "Pause": "Pause", "Pause Reason": "Pause Reason", "Paused": "Paused", "Pending": "Pending", - "Pending for pick": "Pending for pick", + "Pending for pick": "Waiting to be picked", "Pick End Time": "Pick End Time", "Pick Execution Issue Form": "Pick Execution Issue Form", "Pick Order": "Pick Order", "Pick Order Code": "Pick Order Code", - "Pick Order Conso Code": "Pick Order Conso Code", + "Pick Order Conso Code": "Consolidated pick order code", "Pick Order Detail": "Pick Order Detail", "Pick Order Id": "Pick Order Id", "Pick Order No.- Job Order No.- Item": "Pick Order No.- Job Order No.- Item", @@ -293,58 +293,58 @@ "Pick Order Target Date": "Pick Order Target Date", "Pick Start Time": "Pick Start Time", "Pick Time Taken (minutes)": "Pick Time Taken (minutes)", - "Pick order completed successfully!": "Pick order completed successfully!", + "Pick order completed successfully!": "Pick order completed.", "Picked Qty": "Picked Qty", - "Plan Start": "Plan Start", - "Plan Start From": "Plan Start From", - "Plan Start To": "Plan Start To", + "Plan Start": "Planned start", + "Plan Start From": "Planned start from", + "Plan Start To": "Planned start to", "Planning": "Planning", - "Plastic box carton Qty": "Plastic box carton Qty", - "Plastic box carton qty dashboard": "Plastic box carton qty dashboard", - "Plastic box carton qty multi period report": "Plastic box carton qty multi period report", - "Plastic box carton qty report last 7 days": "Plastic box carton qty report last 7 days", - "Plastic box carton qty report this month": "Plastic box carton qty report this month", - "Plastic box carton qty report this year": "Plastic box carton qty report this year", - "Plastic box carton qty usage": "Plastic box carton qty usage", - "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.", - "Please enter at least code or name": "Please enter at least code or name", - "Please enter quantity for all selected items": "Please enter quantity for all selected items", - "Please finish QR code scan and pick order.": "Please finish QR code scan and pick order.", - "Please make sure all required items are picked": "Please make sure all required items are picked", - "Please make sure the qty is enough": "Please make sure the qty is enough", - "Please scan equipment code": "Please scan equipment code", + "Plastic box carton Qty": "Plastic crate qty", + "Plastic box carton qty dashboard": "Plastic crate dashboard", + "Plastic box carton qty multi period report": "Plastic crate qty, multiple periods", + "Plastic box carton qty report last 7 days": "Plastic crate qty (last 7 days)", + "Plastic box carton qty report this month": "Plastic crate qty (this month)", + "Plastic box carton qty report this year": "Plastic crate qty (this year)", + "Plastic box carton qty usage": "Plastic crate qty", + "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "Check nearby for a QR code. Stock may have just been received or transferred.", + "Please enter at least code or name": "Enter a code or a name", + "Please enter quantity for all selected items": "Enter a quantity for every selected item", + "Please finish QR code scan and pick order.": "Finish the QR code scan and the pick order.", + "Please make sure all required items are picked": "Pick every required item first", + "Please make sure the qty is enough": "Insufficient quantity", + "Please scan equipment code": "Scan the equipment code", "Please scan equipment code (optional if not required)": "Please scan equipment code (optional if not required)", - "Please scan operator code": "Please scan operator code", - "Please scan operator code first": "Please scan operator code first", - "Please scan staff no": "Please scan staff no", - "Please scan the item qr code": "Please scan the item qr code", - "Please scan warehouse qr code.": "Please scan warehouse qr code.", + "Please scan operator code": "Scan the operator code", + "Please scan operator code first": "Scan the operator code first", + "Please scan staff no": "Scan the staff number", + "Please scan the item qr code": "Please scan the item QR code", + "Please scan warehouse qr code.": "Please scan the warehouse QR code.", "Please select": "Please select", - "Please select a printer": "Please select a printer", - "Please select a printer first": "Please select a printer first", - "Please select at least one item to submit": "Please select at least one item to submit", - "Please select group and enter quantity for all selected items": "Please select group and enter quantity for all selected items", - "Please select group for all selected items": "Please select group for all selected items", - "Please select item": "Please select item", - "Please select product type": "Please select product type", - "Please select target date": "Please select target date", - "Please select type": "Please select type", - "Please submit the pick order.": "Please submit the pick order.", - "Po Code": "Po Code", + "Please select a printer": "Select a printer", + "Please select a printer first": "Select a printer first", + "Please select at least one item to submit": "Select at least one item", + "Please select group and enter quantity for all selected items": "Choose a group and enter a quantity for every selected item", + "Please select group for all selected items": "Choose a group for every selected item", + "Please select item": "Select an item", + "Please select product type": "Select a product type", + "Please select target date": "Select a target date", + "Please select type": "Select a type", + "Please submit the pick order.": "Submit the pick order.", + "Po Code": "PO code", "Powder Mixture": "Powder Mixture", - "Powder_Mixture": "Powder_Mixture", + "Powder_Mixture": "Powder mix", "Previous page": "Previous page", "Print Pick Record": "Print Pick Record", "Print Quantity": "Print Quantity", - "Printed Successfully.": "Printed Successfully.", + "Printed Successfully.": "Printed successfully.", "Printer": "Printer", "Process": "Process", "Process page summary": "Process page summary", "Processing": "Processing", - "Processing QR code...": "Processing QR code...", + "Processing QR code...": "Reading QR code…", "Processing Status": "Processing Status", "Processing Time": "Processing Time", - "Processing Time (mins)": "Processing Time (mins)", + "Processing Time (mins)": "Processing time (min)", "Processing...": "Processing...", "Product": "Product", "Product Type": "Product Type", @@ -355,7 +355,7 @@ "Production Output Data": "Production Output Data", "Production Output Data Entry": "Production Output Data Entry", "Production Priority": "Production Priority", - "Production Priority required!": "Production Priority required!", + "Production Priority required!": "Production priority is required.", "Production Qty": "Production Qty", "Production Process": "Production Process", "Production Process Information": "Production Process Information", @@ -369,25 +369,25 @@ "Purchase Order": "Purchase Order", "Put Away": "Put Away", "Put Away Scan": "Put Away Scan", - "Put Awayed": "Put Awayed", - "Putaway Detail": "Putaway Detail", + "Put Awayed": "Put away", + "Putaway Detail": "Put-away details", "QC Category": "QC Category", "QC Check Item": "QC Check Item", "QC Check Template": "QC Check Template", "QC Info": "QC Info", - "QR Code Handle": "QR Code Handle", - "QR Code Scan for Lot": "QR Code Scan for Lot", + "QR Code Handle": "QR code print and download", + "QR Code Scan for Lot": "QR code scan for lot", "QR Scan Result:": "QR Scan Result:", - "QR code does not match any item in current orders.": "QR code does not match any item in current orders.", + "QR code does not match any item in current orders.": "This QR code does not match any item on the current orders.", "QR code verified.": "QR code verified.", - "Qc Item": "Qc Item", + "Qc Item": "QC item", "Qty": "Qty", - "Qty Already Picked": "Qty Already Picked", - "Qty cannot be negative": "Qty cannot be negative", - "Qty is not allowed to be greater than required qty": "Qty is not allowed to be greater than required qty", - "Qty is required": "Qty is required", - "Qty must be a whole number": "Qty must be a whole number", - "Qty will submit": "Qty will submit", + "Qty Already Picked": "Already picked", + "Qty cannot be negative": "Quantity cannot be negative", + "Qty is not allowed to be greater than required qty": "Quantity cannot be greater than the required quantity", + "Qty is required": "Quantity is required", + "Qty must be a whole number": "Quantity must be a whole number", + "Qty will submit": "Quantity to submit", "Quality Check": "Quality Check", "Quantity": "Quantity", "R&D": "R&D", @@ -401,14 +401,14 @@ "Remaining Available Qty": "Remaining Available Qty", "Remaining Time (min)": "Remaining Time (min)", "Remark": "Remark", - "Remember plan start as default": "Remember plan start as default", + "Remember plan start as default": "Remember this planned start time", "Repair": "Repair", - "Req. Qty": "Req. Qty", + "Req. Qty": "Required qty", "Required Qty": "Required Qty", "Reset": "Reset", "Route": "Route", "Router": "Router", - "Row per page": "Row per page", + "Row per page": "Rows per page", "Rows per page": "Rows per page", "SEQ": "SEQ", "STF": "STF", @@ -424,11 +424,11 @@ "Search": "Search", "Search Criteria": "Search Criteria", "Search Items": "Search Items", - "Search Job Order/ Create Job Order": "Search Job Order/ Create Job Order", + "Search Job Order/ Create Job Order": "Search Job Order / Create Job Order", "Search Results": "Search Results", - "Second Scan Completed": "Second Scan Completed", - "Second Scan Pending": "Second Scan Pending", - "Second Scan Status": "Second Scan Status", + "Second Scan Completed": "Material match completed", + "Second Scan Pending": "Material match pending", + "Second Scan Status": "Material match status", "Select Another Bag Lot": "Select Another Bag Lot", "Select Bag": "Select Bag", "Select Date": "Select Date", @@ -437,18 +437,18 @@ "Select a printer": "Select a printer", "Select warehouse": "Select warehouse", "Selected": "Selected", - "Selected items will join above created group": "Selected items will join above created group", + "Selected items will join above created group": "Selected items will be added to the group above.", "Seq": "Seq", "Seq No": "Seq No", "Seq No Remark": "Seq No Remark", "Seq:": "Seq:", "Sequence": "Sequence", "Setup Time": "Setup Time", - "Setup Time (mins)": "Setup Time (mins)", + "Setup Time (mins)": "Setup time (min)", "Shop Address": "Shop Address", "Shop ID": "Shop ID", "Shop Name": "Shop Name", - "Shop PO Code": "Shop PO Code", + "Shop PO Code": "Shop PO code", "Sign out": "Sign out", "Skip": "Skip", "Staff No": "Staff No", @@ -464,16 +464,16 @@ "Step": "Step", "Step Information": "Step Information", "Step Name": "Step Name", - "Still need to pick": "Still need to pick", + "Still need to pick": "Still to pick", "Stock Available": "Stock Available", - "Stock Req. Qty": "Stock Req. Qty", + "Stock Req. Qty": "Stock qty required", "Stock Status": "Stock Status", "Stock UOM": "Stock UOM", "Stock Unit": "Stock Unit", "Stop": "Stop", "Stop QR Scan": "Stop QR Scan", "Stop Scan": "Stop Scan", - "Store ID": "Store ID", + "Store ID": "Shop ID", "Storing": "Storing", "Submit": "Submit", "Submit & Start": "Submit & Start", @@ -488,31 +488,31 @@ "Target Date To": "Target Date To", "Target Production Date": "Target Production Date", "Task Template": "Task Template", - "The input is not the same as the expected lot number.": "The input is not the same as the expected lot number.", - "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?": "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?", - "This form is for reporting issues only. You must report either missing items or bad items.": "This form is for reporting issues only. You must report either missing items or bad items.", - "This is last lot, so no available lot.": "This is last lot, so no available lot.", - "This lot is rejected, please scan another lot.": "This lot is rejected, please scan another lot.", + "The input is not the same as the expected lot number.": "The lot number does not match the expected lot.", + "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?": "This is the right item, but a different lot. Use this lot anyway?", + "This form is for reporting issues only. You must report either missing items or bad items.": "Use this form to report a problem. Enter missing items or damaged items.", + "This is last lot, so no available lot.": "This is the last lot, so no other lot is available.", + "This lot is rejected, please scan another lot.": "This lot was rejected. Scan another lot.", "This month": "This month", - "This order is insufficient, please pick another lot.": "This order is insufficient, please pick another lot.", + "This order is insufficient, please pick another lot.": "There is not enough quantity on this lot. Pick another lot.", "This year": "This year", "Ticket No.": "Ticket No.", "Time": "Time", - "Time Information(mins)": "Time Information(mins)", + "Time Information(mins)": "Time (minutes)", "Time Remaining": "Time Remaining", "Time used": "Time used", "Timer Paused": "Timer Paused", "Today": "Today", "Total": "Total", - "Total (Verified + Bad + Missing) must equal Required quantity": "Total (Verified + Bad + Missing) must equal Required quantity", + "Total (Verified + Bad + Missing) must equal Required quantity": "Verified + damaged + missing must equal the required quantity", "Total Processing Time": "Total Processing Time", "Total Steps": "Total Steps", "Total lines: ": "Total lines: ", "Total pick orders": "Total pick orders", - "Total plastic box carton qty": "Total plastic box carton qty", + "Total plastic box carton qty": "Total plastic crate qty", "Total processes": "Total processes", "Truck No.": "Truck No.", - "Two Days Ago": "Two Days Ago", + "Two Days Ago": "Two days ago", "Type": "Type", "Unable to get user ID": "Unable to get user ID", "Unassigned Job Orders": "Unassigned Job Orders", @@ -520,138 +520,138 @@ "Unknown": "Unknown", "Unknown error": "Unknown error", "Unknown error: ": "Unknown error: ", - "UoM": "UoM", + "UoM": "Sales UOM", "Update Estimated Production Date": "Update Estimated Production Date", "Update Job Order": "Update Job Order", "Update Production Priority": "Update Production Priority", "Update Time Information": "Update Time Information", - "Update your suggested lot to the this scanned lot": "Update your suggested lot to the this scanned lot", - "User not found with staffNo:": "User not found with staffNo:", - "Validation failed. Please check operator and equipment.": "Validation failed. Please check operator and equipment.", + "Update your suggested lot to the this scanned lot": "Update the suggested lot to this scanned lot.", + "User not found with staffNo:": "No user found for staff no.:", + "Validation failed. Please check operator and equipment.": "Check the operator and the equipment, then try again.", "Verified Qty": "Verified Qty", - "Verified quantity cannot exceed received quantity": "Verified quantity cannot exceed received quantity", + "Verified quantity cannot exceed received quantity": "Verified quantity cannot be more than the received quantity", "Verified successfully!": "Verified successfully!", "View": "View", "View Details": "View Details", - "View item In-out And inventory Ledger": "View item In-out And inventory Ledger", + "View item In-out And inventory Ledger": "Item In/Out and Inventory Ledger", "WIP": "WIP", - "Wait Time [minutes]": "Wait Time [minutes]", + "Wait Time [minutes]": "Wait time (min)", "Warehouse": "Warehouse", "Yesterday": "Yesterday", - "You need to enter a number": "You need to enter a number", - "acceptQty": "acceptQty", - "acceptQty must not greater than": "acceptQty must not greater than", - "acceptedQty": "acceptedQty", - "acceptedQty must not greater than": "acceptedQty must not greater than", - "acceptedWeight": "acceptedWeight", - "bind": "bind", - "bomWarn_close": "bomWarn_close", - "bomWarn_copyAll": "bomWarn_copyAll", - "bomWarn_empty": "bomWarn_empty", - "bomWarn_issue_MISSING_BOM_CODE": "bomWarn_issue_MISSING_BOM_CODE", - "bomWarn_issue_MISSING_BOM_NAME": "bomWarn_issue_MISSING_BOM_NAME", - "bomWarn_issue_MISSING_ITEM": "bomWarn_issue_MISSING_ITEM", - "bomWarn_issue_MISSING_SALES_UOM": "bomWarn_issue_MISSING_SALES_UOM", - "bomWarn_issue_MISSING_STOCK_UOM": "bomWarn_issue_MISSING_STOCK_UOM", - "bomWarn_issue_MISSING_STOCK_UOM_CONVERSION": "bomWarn_issue_MISSING_STOCK_UOM_CONVERSION", - "bomWarn_issue_MISSING_UOM_CONVERSION": "bomWarn_issue_MISSING_UOM_CONVERSION", - "bomWarn_loadFailed": "bomWarn_loadFailed", - "bomWarn_refresh": "bomWarn_refresh", - "bomWarn_refreshing": "bomWarn_refreshing", - "bomWarn_rowBomId": "bomWarn_rowBomId", - "bomWarn_rowItemId": "bomWarn_rowItemId", - "bomWarn_title": "bomWarn_title", - "bomWarn_tooltipHas": "bomWarn_tooltipHas", - "bomWarn_tooltipNone": "bomWarn_tooltipNone", - "cancel": "cancel", - "cancelled": "cancelled", - "complete jo": "complete jo", + "You need to enter a number": "Enter a number", + "acceptQty": "Accepted qty", + "acceptQty must not greater than": "Accepted quantity cannot be greater than", + "acceptedQty": "Accepted qty", + "acceptedQty must not greater than": "Accepted quantity cannot be greater than", + "acceptedWeight": "Accepted weight", + "bind": "Bind", + "bomWarn_close": "Close", + "bomWarn_copyAll": "Copy list", + "bomWarn_empty": "No BOM data issues.", + "bomWarn_issue_MISSING_BOM_CODE": "BOM code is empty", + "bomWarn_issue_MISSING_BOM_NAME": "BOM name is empty", + "bomWarn_issue_MISSING_ITEM": "The linked item is missing or deleted", + "bomWarn_issue_MISSING_SALES_UOM": "Item has no sales unit", + "bomWarn_issue_MISSING_STOCK_UOM": "Item has no stock unit. QC may fail.", + "bomWarn_issue_MISSING_STOCK_UOM_CONVERSION": "Stock unit conversion is missing or deleted. QC may fail.", + "bomWarn_issue_MISSING_UOM_CONVERSION": "Sales unit conversion is missing or deleted", + "bomWarn_loadFailed": "Could not load the BOM issue list. Try again later.", + "bomWarn_refresh": "Check again", + "bomWarn_refreshing": "Checking…", + "bomWarn_rowBomId": "BOM ID", + "bomWarn_rowItemId": "Item ID", + "bomWarn_title": "BOM data issues", + "bomWarn_tooltipHas": "{{count}} BOM data issue(s)", + "bomWarn_tooltipNone": "No BOM data issues", + "cancel": "Cancel", + "cancelled": "Cancelled", + "complete jo": "Complete job order", "completed": "Completed", "consolidated": "Consolidated", "assigned": "Assigned", "released": "Released", "picking": "Picking", - "completed Job Order pick orders with Matching": "completed Job Order pick orders with Matching", - "completed Job Order pick orders with matching": "completed Job Order pick orders with matching", - "consumables": "consumables", - "create": "create", - "edit": "edit", - "enter a qty": "enter a qty", - "equipmentType": "equipmentType", + "completed Job Order pick orders with Matching": "Job orders with picking and material match completed", + "completed Job Order pick orders with matching": "Job orders with picking and material match completed", + "consumables": "Consumables", + "create": "Create", + "edit": "Edit", + "enter a qty": "Enter a quantity", + "equipmentType": "Equipment type", "escalation": "escalation", - "escalation processing": "escalation processing", - "expiryDate": "expiryDate", - "failedQty": "failedQty", - "fg": "fg", - "finishedGood": "finishedGood", - "handler": "handler", + "escalation processing": "Escalation in progress", + "expiryDate": "Expiry date", + "failedQty": "Failed qty", + "fg": "FG", + "finishedGood": "Finished goods", + "handler": "Handler", "hr": "hr", - "hrs": "hrs", + "hrs": "hr", "id": "id", - "inProgress": "inProgress", - "in_progress": "in_progress", - "is expired. Please check around have available QR code or not.": "is expired. Please check around have available QR code or not.", - "is unavable. Please check around have available QR code or not.": "is unavable. Please check around have available QR code or not.", + "inProgress": "In progress", + "in_progress": "In progress", + "is expired. Please check around have available QR code or not.": "This lot is expired. Check nearby for an available QR code.", + "is unavable. Please check around have available QR code or not.": "This lot is unavailable. Check nearby for an available QR code.", "issue": "issue", - "issue remark": "issue remark", + "issue remark": "Issue description", "item": "item", - "itemName": "itemName", - "itemNo": "itemNo", - "items": "items", - "items completed": "items completed", - "jodetail": "jodetail", - "mat": "mat", + "itemName": "Product name", + "itemNo": "Finished goods code", + "items": "Items", + "items completed": "Items completed", + "jodetail": "Job order", + "mat": "Material", "min": "min", - "minimal value is 1": "minimal value is 1", - "mins": "mins", + "minimal value is 1": "Minimum value is 1", + "mins": "min", "minutes": "minutes", - "non-consumables": "non-consumables", - "not default warehosue": "not default warehosue", - "packaging": "packaging", - "paused": "paused", - "pending": "pending", + "non-consumables": "Non-consumables", + "not default warehosue": "Not the default warehouse", + "packaging": "Packaging", + "paused": "Paused", + "pending": "Pending", "pendingQC": "Pending QC", "planning": "Planning", "processing": "Processing", "scanned": "Scanned", - "printQty": "printQty", - "process epqc": "process epqc", - "process stockIn": "process stockIn", - "productionDate": "productionDate", - "qcCategory": "qcCategory", - "qcItem": "qcItem", - "qcResult": "qcResult", - "qty": "qty", - "qty is not allowed to be greater than picked qty": "qty is not allowed to be greater than picked qty", - "qty is not allowed to be greater than remaining available qty": "qty is not allowed to be greater than remaining available qty", - "qty is not allowed to be greater than required qty": "qty is not allowed to be greater than required qty", - "qty is required": "qty is required", - "receivedQty": "receivedQty", - "release jo": "release jo", - "remarks": "remarks", - "reset": "reset", - "scan picked material": "scan picked material", + "printQty": "Print qty", + "process epqc": "Run finished-goods QC", + "process stockIn": "Receive stock", + "productionDate": "Production date", + "qcCategory": "QC template", + "qcItem": "QC item", + "qcResult": "QC result", + "qty": "Qty", + "qty is not allowed to be greater than picked qty": "Quantity cannot be greater than the picked quantity", + "qty is not allowed to be greater than remaining available qty": "Quantity cannot be greater than the remaining available quantity", + "qty is not allowed to be greater than required qty": "Quantity cannot be greater than the required quantity", + "qty is required": "Quantity is required", + "receivedQty": "Received qty", + "release jo": "Job order details", + "remarks": "Remarks", + "reset": "Reset", + "scan picked material": "Scan to confirm the pick", "scheduling": "scheduling", - "second Scan Status": "second Scan Status", - "select qc": "select qc", - "seq": "seq", - "settings": "settings", - "sfg": "sfg", - "stock in information": "stock in information", - "stopped": "stopped", + "second Scan Status": "Material match status", + "select qc": "Select QC", + "seq": "Seq", + "settings": "Settings", + "sfg": "Semi-finished", + "stock in information": "Stock-in details", + "stopped": "Stopped", "storing": "storing", - "submit": "submit", - "submitting": "submitting", - "success": "success", - "supervisor": "supervisor", - "targetDate": "targetDate", + "submit": "Submit", + "submitting": "Submitting", + "success": "Success", + "supervisor": "Supervisor", + "targetDate": "Required date", "type": "type", - "uom": "uom", - "update production priority": "update production priority", - "update qc info": "update qc info", - "update success": "update success", - "value must be a number": "value must be a number", - "view putaway": "view putaway", - "view stockin": "view stockin", - "warehouse": "warehouse" + "uom": "UOM", + "update production priority": "Update production priority", + "update qc info": "Update QC details", + "update success": "Updated successfully", + "value must be a number": "Enter a number", + "view putaway": "View put-away details", + "view stockin": "QC", + "warehouse": "Warehouse" } diff --git a/src/i18n/en/login.json b/src/i18n/en/login.json index 4b0d3143..72bb737f 100644 --- a/src/i18n/en/login.json +++ b/src/i18n/en/login.json @@ -1,11 +1,11 @@ { - "Invalid username or password.": "Invalid username or password.", + "Invalid username or password.": "That username or password is not correct.", "Login": "Login", "Password": "Password", - "Please enter a password": "Please enter a password", - "Please enter a username": "Please enter a username", + "Please enter a password": "Enter a password", + "Please enter a username": "Enter a username", "Sign In": "Sign In", - "Something went wrong. Please try again later.": "Something went wrong. Please try again later.", + "Something went wrong. Please try again later.": "Something went wrong. Try again later.", "Username": "Username", - "toggle password visibility": "Toggle password visibility" + "toggle password visibility": "Show or hide password" } diff --git a/src/i18n/en/m18ImportTesting.json b/src/i18n/en/m18ImportTesting.json index 1df10115..c0658d88 100644 --- a/src/i18n/en/m18ImportTesting.json +++ b/src/i18n/en/m18ImportTesting.json @@ -1,9 +1,9 @@ { - "testing sections tabs": "testing sections tabs", + "testing sections tabs": "Testing section tabs", "Import Delivery Order": "Import Delivery Order", "Import Do": "Import Do", "Import Master Data": "Import Master Data", - "Import Po": "Import Po", + "Import Po": "Import PO", "Import Pq": "Import Pq", "Import Purchase Order": "Import Purchase Order", "Import Purchase Quotation": "Import Purchase Quotation", diff --git a/src/i18n/en/m18Sync.json b/src/i18n/en/m18Sync.json index aa645ad1..f6285114 100644 --- a/src/i18n/en/m18Sync.json +++ b/src/i18n/en/m18Sync.json @@ -8,11 +8,11 @@ "tabProduct": "4. Product", "sectionPo": "M18 Purchase Order — sync by code", "sectionDo": "M18 Delivery Order — sync by code", - "sectionDoExtra": "M18 Delivery Order — Extra (isExtra)", + "sectionDoExtra": "M18 delivery order — extra", "sectionProduct": "M18 Product / Material — sync by code", "labelPoCode": "PO Code", "labelDoCode": "DO / Shop PO Code", - "labelDoExtraCode": "DO / Shop PO Code (Extra)", + "labelDoExtraCode": "Delivery order / shop PO (extra)", "labelProductCode": "Item / Product Code", "labelSyncResult": "Sync Result", "btnSyncPo": "Sync PO from M18", @@ -36,9 +36,9 @@ "summaryNotFound": "{{count}} order(s) not found in M18 ({{codes}})", "summaryExists": "{{count}} order(s) already exist in system ({{codes}})", "summaryFail": "{{count}} order(s) sync failed ({{codes}})", - "alertEnterPoCode": "Please enter PO code.", - "alertEnterDoCode": "Please enter DO / shop PO code.", - "alertEnterDoExtraCode": "Please enter DO / shop PO code(s) (Extra).", + "alertEnterPoCode": "Enter a purchase order code.", + "alertEnterDoCode": "Enter a delivery order or shop PO code.", + "alertEnterDoExtraCode": "Enter the extra delivery order or shop PO code.", "alertEnterAtLeastOne": "Please enter at least one code.", "alertEnterProductCode": "Please enter M18 item / product code.", "alertSyncFailed": "Sync failed: {{status}}", diff --git a/src/i18n/en/masterDataIssue.json b/src/i18n/en/masterDataIssue.json index e1bfdc82..6177f598 100644 --- a/src/i18n/en/masterDataIssue.json +++ b/src/i18n/en/masterDataIssue.json @@ -1,133 +1,133 @@ { "Current Stock": "Current Stock", - "masterDataIssue": "masterDataIssue", - "masterDataIssue_BOM_MATERIAL_BASE_UOM_MISMATCH": "masterDataIssue_BOM_MATERIAL_BASE_UOM_MISMATCH", - "masterDataIssue_BOM_MATERIAL_MISSING_ITEM": "masterDataIssue_BOM_MATERIAL_MISSING_ITEM", - "masterDataIssue_BOM_MATERIAL_SALES_UOM_MISMATCH": "masterDataIssue_BOM_MATERIAL_SALES_UOM_MISMATCH", - "masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH": "masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH", - "masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID": "BOM material UOM reference invalid or deleted", - "masterDataIssue_BOM_ITEM_CODE_MISMATCH": "BOM code does not match linked item", - "masterDataIssue_BOM_ITEM_NAME_MISMATCH": "BOM product name does not match linked item", + "masterDataIssue": "BOM / item unit issues", + "masterDataIssue_BOM_MATERIAL_BASE_UOM_MISMATCH": "BOM material base unit does not match the item master", + "masterDataIssue_BOM_MATERIAL_MISSING_ITEM": "BOM material item is missing or deleted", + "masterDataIssue_BOM_MATERIAL_SALES_UOM_MISMATCH": "BOM material sales unit does not match the item master", + "masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH": "BOM material stock unit does not match the item master", + "masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID": "BOM material unit reference is invalid or deleted", + "masterDataIssue_BOM_ITEM_CODE_MISMATCH": "BOM code does not match the linked item", + "masterDataIssue_BOM_ITEM_NAME_MISMATCH": "BOM product name does not match the linked item", "masterDataIssue_col_linked_item": "Linked item", "masterDataIssue_col_product_name": "Product name", - "masterDataIssue_line_itemCodeMismatch": "{{bom}}: should link item \"{{expected}}\", actually \"{{actual}}\"", + "masterDataIssue_line_itemCodeMismatch": "{{bom}}: should link to item \"{{expected}}\", but links to \"{{actual}}\"", "masterDataIssue_line_itemNameMismatch": "{{bom}}: BOM/Excel name \"{{actual}}\", item master \"{{expected}}\"", - "masterDataIssue_BOM_OUTPUT_UOM_MISMATCH_SALES": "masterDataIssue_BOM_OUTPUT_UOM_MISMATCH_SALES", - "masterDataIssue_BOM_OUTPUT_UOM_TEXT_DRIFT": "masterDataIssue_BOM_OUTPUT_UOM_TEXT_DRIFT", - "masterDataIssue_DELETED_BASE_UOM": "masterDataIssue_DELETED_BASE_UOM", - "masterDataIssue_DELETED_PURCHASE_UOM": "masterDataIssue_DELETED_PURCHASE_UOM", - "masterDataIssue_DELETED_SALES_UOM": "masterDataIssue_DELETED_SALES_UOM", - "masterDataIssue_DELETED_STOCK_UOM": "masterDataIssue_DELETED_STOCK_UOM", - "masterDataIssue_MISSING_BASE_UOM": "masterDataIssue_MISSING_BASE_UOM", - "masterDataIssue_MISSING_BASE_UOM_CONVERSION": "masterDataIssue_MISSING_BASE_UOM_CONVERSION", - "masterDataIssue_MISSING_BOM_CODE": "masterDataIssue_MISSING_BOM_CODE", - "masterDataIssue_MISSING_BOM_NAME": "masterDataIssue_MISSING_BOM_NAME", - "masterDataIssue_MISSING_ITEM": "masterDataIssue_MISSING_ITEM", - "masterDataIssue_MISSING_PICKING_UOM": "masterDataIssue_MISSING_PICKING_UOM", - "masterDataIssue_MISSING_PICKING_UOM_CONVERSION": "masterDataIssue_MISSING_PICKING_UOM_CONVERSION", - "masterDataIssue_MISSING_PURCHASE_UOM": "masterDataIssue_MISSING_PURCHASE_UOM", - "masterDataIssue_MISSING_PURCHASE_UOM_CONVERSION": "masterDataIssue_MISSING_PURCHASE_UOM_CONVERSION", - "masterDataIssue_MISSING_SALES_UOM": "masterDataIssue_MISSING_SALES_UOM", - "masterDataIssue_MISSING_SALES_UOM_CONVERSION": "masterDataIssue_MISSING_SALES_UOM_CONVERSION", - "masterDataIssue_MISSING_STOCK_UOM": "masterDataIssue_MISSING_STOCK_UOM", - "masterDataIssue_MISSING_STOCK_UOM_CONVERSION": "masterDataIssue_MISSING_STOCK_UOM_CONVERSION", - "masterDataIssue_MISSING_UOM_CONVERSION": "masterDataIssue_MISSING_UOM_CONVERSION", - "masterDataIssue_MULTIPLE_BASE_UOM": "masterDataIssue_MULTIPLE_BASE_UOM", - "masterDataIssue_MULTIPLE_PICKING_UOM": "masterDataIssue_MULTIPLE_PICKING_UOM", - "masterDataIssue_MULTIPLE_PURCHASE_UOM": "masterDataIssue_MULTIPLE_PURCHASE_UOM", - "masterDataIssue_MULTIPLE_SALES_UOM": "masterDataIssue_MULTIPLE_SALES_UOM", - "masterDataIssue_MULTIPLE_STOCK_UOM": "masterDataIssue_MULTIPLE_STOCK_UOM", - "masterDataIssue_bomMore": "masterDataIssue_bomMore", - "masterDataIssue_close": "masterDataIssue_close", - "masterDataIssue_col_actions": "masterDataIssue_col_actions", - "masterDataIssue_col_actual": "masterDataIssue_col_actual", + "masterDataIssue_BOM_OUTPUT_UOM_MISMATCH_SALES": "BOM output unit does not match the finished goods sales unit", + "masterDataIssue_BOM_OUTPUT_UOM_TEXT_DRIFT": "BOM output unit text does not match the UOM master", + "masterDataIssue_DELETED_BASE_UOM": "Base unit was deleted", + "masterDataIssue_DELETED_PURCHASE_UOM": "Purchase unit was deleted", + "masterDataIssue_DELETED_SALES_UOM": "Sales unit was deleted", + "masterDataIssue_DELETED_STOCK_UOM": "Stock unit was deleted", + "masterDataIssue_MISSING_BASE_UOM": "Base unit is missing", + "masterDataIssue_MISSING_BASE_UOM_CONVERSION": "Base unit conversion is missing or deleted", + "masterDataIssue_MISSING_BOM_CODE": "BOM code is empty", + "masterDataIssue_MISSING_BOM_NAME": "BOM name is empty", + "masterDataIssue_MISSING_ITEM": "The linked item is missing or deleted", + "masterDataIssue_MISSING_PICKING_UOM": "Picking unit is missing", + "masterDataIssue_MISSING_PICKING_UOM_CONVERSION": "Picking unit conversion is missing or deleted", + "masterDataIssue_MISSING_PURCHASE_UOM": "Purchase unit is missing", + "masterDataIssue_MISSING_PURCHASE_UOM_CONVERSION": "Purchase unit conversion is missing or deleted", + "masterDataIssue_MISSING_SALES_UOM": "Sales unit is missing", + "masterDataIssue_MISSING_SALES_UOM_CONVERSION": "Sales unit conversion is missing or deleted", + "masterDataIssue_MISSING_STOCK_UOM": "Stock unit is missing", + "masterDataIssue_MISSING_STOCK_UOM_CONVERSION": "Stock unit conversion is missing or deleted", + "masterDataIssue_MISSING_UOM_CONVERSION": "Unit conversion is missing or deleted", + "masterDataIssue_MULTIPLE_BASE_UOM": "More than one base unit", + "masterDataIssue_MULTIPLE_PICKING_UOM": "More than one picking unit", + "masterDataIssue_MULTIPLE_PURCHASE_UOM": "More than one purchase unit", + "masterDataIssue_MULTIPLE_SALES_UOM": "More than one sales unit", + "masterDataIssue_MULTIPLE_STOCK_UOM": "More than one stock unit", + "masterDataIssue_bomMore": " and {{count}} more", + "masterDataIssue_close": "Close", + "masterDataIssue_col_actions": "Actions", + "masterDataIssue_col_actual": "Actual", "masterDataIssue_col_bom": "BOM", "masterDataIssue_col_bom_uom": "BOM UOM", - "masterDataIssue_col_expected": "masterDataIssue_col_expected", - "masterDataIssue_col_issue": "masterDataIssue_col_issue", - "masterDataIssue_col_item": "masterDataIssue_col_item", + "masterDataIssue_col_expected": "Expected", + "masterDataIssue_col_issue": "Issue", + "masterDataIssue_col_item": "Item", "masterDataIssue_col_item_uom": "M18 UOM", - "masterDataIssue_col_problem": "masterDataIssue_col_problem", - "masterDataIssue_col_scope": "masterDataIssue_col_scope", - "masterDataIssue_col_subject": "masterDataIssue_col_subject", - "masterDataIssue_col_summary": "masterDataIssue_col_summary", - "masterDataIssue_copy": "masterDataIssue_copy", - "masterDataIssue_count": "masterDataIssue_count", - "masterDataIssue_detail_subtitle": "masterDataIssue_detail_subtitle", - "masterDataIssue_detail_uomBase": "masterDataIssue_detail_uomBase", - "masterDataIssue_detail_uomBoth": "masterDataIssue_detail_uomBoth", - "masterDataIssue_detail_uomSales": "masterDataIssue_detail_uomSales", - "masterDataIssue_detail_uomStock": "masterDataIssue_detail_uomStock", - "masterDataIssue_detail_usedInBom": "masterDataIssue_detail_usedInBom", - "masterDataIssue_empty": "masterDataIssue_empty", - "masterDataIssue_fgItem": "masterDataIssue_fgItem", - "masterDataIssue_filter_all": "masterDataIssue_filter_all", - "masterDataIssue_filter_type": "masterDataIssue_filter_type", - "masterDataIssue_group_count": "masterDataIssue_group_count", - "masterDataIssue_issueCount": "masterDataIssue_issueCount", - "masterDataIssue_line_generic": "masterDataIssue_line_generic", - "masterDataIssue_line_itemGeneric": "masterDataIssue_line_itemGeneric", - "masterDataIssue_line_itemMissing": "masterDataIssue_line_itemMissing", - "masterDataIssue_line_missingUnits": "masterDataIssue_line_missingUnits", - "masterDataIssue_line_outputText": "masterDataIssue_line_outputText", - "masterDataIssue_line_outputUom": "masterDataIssue_line_outputUom", - "masterDataIssue_line_pairBase": "masterDataIssue_line_pairBase", - "masterDataIssue_line_pairBoth": "masterDataIssue_line_pairBoth", - "masterDataIssue_line_pairSales": "masterDataIssue_line_pairSales", - "masterDataIssue_line_pairStock": "masterDataIssue_line_pairStock", - "masterDataIssue_line_problemOnly": "{{bom}}:{{problem}}", - "masterDataIssue_line_uomBoth": "masterDataIssue_line_uomBoth", - "masterDataIssue_line_uomSales": "masterDataIssue_line_uomSales", - "masterDataIssue_line_uomStock": "masterDataIssue_line_uomStock", - "masterDataIssue_loadFailed": "masterDataIssue_loadFailed", - "masterDataIssue_material": "masterDataIssue_material", - "masterDataIssue_materialUsedInBom": "masterDataIssue_materialUsedInBom", - "masterDataIssue_modifiedAt": "masterDataIssue_modifiedAt", - "masterDataIssue_nav": "masterDataIssue_nav", - "masterDataIssue_pageTitle": "masterDataIssue_pageTitle", - "masterDataIssue_refresh": "masterDataIssue_refresh", - "masterDataIssue_refreshing": "masterDataIssue_refreshing", - "masterDataIssue_scope_BOM": "masterDataIssue_scope_BOM", - "masterDataIssue_scope_BOM_MATERIAL": "masterDataIssue_scope_BOM_MATERIAL", - "masterDataIssue_scope_ITEM": "masterDataIssue_scope_ITEM", - "masterDataIssue_search": "masterDataIssue_search", + "masterDataIssue_col_problem": "Issue", + "masterDataIssue_col_scope": "Scope", + "masterDataIssue_col_subject": "Subject", + "masterDataIssue_col_summary": "Summary", + "masterDataIssue_copy": "Copy list", + "masterDataIssue_count": "{{count}} issues", + "masterDataIssue_detail_subtitle": "{{count}} issues", + "masterDataIssue_detail_uomBase": "Base unit does not match the item master", + "masterDataIssue_detail_uomBoth": "Sales and stock units do not match the item master", + "masterDataIssue_detail_uomSales": "Sales unit does not match the item master", + "masterDataIssue_detail_uomStock": "Stock unit does not match the item master", + "masterDataIssue_detail_usedInBom": "Used on BOM", + "masterDataIssue_empty": "No issues.", + "masterDataIssue_fgItem": "Finished goods item", + "masterDataIssue_filter_all": "All", + "masterDataIssue_filter_type": "Type", + "masterDataIssue_group_count": "{{groups}} records · {{issues}} issues", + "masterDataIssue_issueCount": "{{count}}", + "masterDataIssue_line_generic": "{{bom}}: {{problem}} (expected \"{{expected}}\", actual \"{{actual}}\")", + "masterDataIssue_line_itemGeneric": "{{problem}} (expected \"{{expected}}\", actual \"{{actual}}\")", + "masterDataIssue_line_itemMissing": "Missing: {{units}}", + "masterDataIssue_line_missingUnits": "{{bom}}: missing {{units}}", + "masterDataIssue_line_outputText": "{{bom}}: output unit text should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_line_outputUom": "{{bom}}: output unit should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_line_pairBase": "Base unit should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_line_pairBoth": "Sales/stock unit should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_line_pairSales": "Sales unit should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_line_pairStock": "Stock unit should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_line_problemOnly": "{{bom}}: {{problem}}", + "masterDataIssue_line_uomBoth": "{{bom}}: sales/stock unit should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_line_uomSales": "{{bom}}: sales unit should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_line_uomStock": "{{bom}}: stock unit should be \"{{expected}}\", BOM has \"{{actual}}\"", + "masterDataIssue_loadFailed": "Could not load the issue list. Try again later.", + "masterDataIssue_material": "Material", + "masterDataIssue_materialUsedInBom": "Used on BOM: {{codes}}", + "masterDataIssue_modifiedAt": "Modified", + "masterDataIssue_nav": "BOM / item unit issues", + "masterDataIssue_pageTitle": "BOM / item unit issues", + "masterDataIssue_refresh": "Check again", + "masterDataIssue_refreshing": "Checking…", + "masterDataIssue_scope_BOM": "BOM header", + "masterDataIssue_scope_BOM_MATERIAL": "BOM material", + "masterDataIssue_scope_ITEM": "Item", + "masterDataIssue_search": "Search", "masterDataIssue_tab_bom": "BOM", - "masterDataIssue_tab_item": "masterDataIssue_tab_item", - "masterDataIssue_unit_active": "masterDataIssue_unit_active", - "masterDataIssue_unit_base": "masterDataIssue_unit_base", - "masterDataIssue_unit_inactive": "masterDataIssue_unit_inactive", - "masterDataIssue_unit_missing": "masterDataIssue_unit_missing", - "masterDataIssue_unit_output": "masterDataIssue_unit_output", - "masterDataIssue_unit_picking": "masterDataIssue_unit_picking", - "masterDataIssue_unit_purchase": "masterDataIssue_unit_purchase", - "masterDataIssue_unit_sales": "masterDataIssue_unit_sales", - "masterDataIssue_unit_stock": "masterDataIssue_unit_stock", - "masterDataIssue_usedInBom": "masterDataIssue_usedInBom", - "masterDataIssue_viewDetail": "masterDataIssue_viewDetail", + "masterDataIssue_tab_item": "Items", + "masterDataIssue_unit_active": "In use", + "masterDataIssue_unit_base": "Base unit", + "masterDataIssue_unit_inactive": "Inactive", + "masterDataIssue_unit_missing": "No unit", + "masterDataIssue_unit_output": "Output unit", + "masterDataIssue_unit_picking": "Picking unit", + "masterDataIssue_unit_purchase": "Purchase unit", + "masterDataIssue_unit_sales": "Sales unit", + "masterDataIssue_unit_stock": "Stock unit", + "masterDataIssue_usedInBom": "Used on {{count}} BOM(s): {{codes}}", + "masterDataIssue_viewDetail": "View details", "masterDataIssue_align_preview": "Preview fix (align to M18)", "masterDataIssue_align_row": "Align to M18", - "masterDataIssue_align_title": "Preview: align BOM UOM to M18", - "masterDataIssue_align_info_m18": "If M18 item master is correct: use this to align BOM units and quantities. Header output qty defaults unchanged; material sale/stock/base qty recalculate via item_uom when edited. Recipe qty is independent in v1.", - "masterDataIssue_align_info_excel": "If BOM Excel is correct: do not apply this fix; update item UOM in M18 sync instead.", - "masterDataIssue_align_summary": "Fixable: {{headers}} header(s) · {{materials}} material(s) · {{skipped}} skipped", - "masterDataIssue_align_tab_headers": "BOM header ({{count}})", + "masterDataIssue_align_title": "Preview: align BOM units to M18", + "masterDataIssue_align_info_m18": "Use this when the M18 item master is correct. It aligns BOM units and quantities to M18. Header output quantity stays as it is. Material sales, stock, and base quantities recalculate from the item units. Recipe quantity is separate.", + "masterDataIssue_align_info_excel": "If the BOM Excel file is the correct source, do not apply this. Update the item units in M18 sync instead.", + "masterDataIssue_align_summary": "Can fix: {{headers}} header(s) · {{materials}} material(s) · {{skipped}} skipped", + "masterDataIssue_align_tab_headers": "BOM headers ({{count}})", "masterDataIssue_align_tab_materials": "BOM materials ({{count}})", "masterDataIssue_align_tab_skipped": "Skipped ({{count}})", - "masterDataIssue_align_none_headers": "No fixable BOM headers.", - "masterDataIssue_align_none_materials": "No fixable BOM materials.", + "masterDataIssue_align_none_headers": "No BOM headers can be fixed.", + "masterDataIssue_align_none_materials": "No BOM materials can be fixed.", "masterDataIssue_align_field": "Field", "masterDataIssue_align_after": "After fix", "masterDataIssue_align_outputQty": "Output qty", "masterDataIssue_align_recipeQty": "Recipe qty", - "masterDataIssue_align_confirm": "I confirm M18 item master is the source of truth", + "masterDataIssue_align_confirm": "I confirm the M18 item master is the source of truth", "masterDataIssue_align_apply": "Apply fix", "masterDataIssue_align_applying": "Applying…", - "masterDataIssue_align_loadFailed": "Failed to load alignment preview.", - "masterDataIssue_align_applyFailed": "Apply failed. Please try again.", + "masterDataIssue_align_loadFailed": "Could not load the preview.", + "masterDataIssue_align_applyFailed": "Could not apply the fix. Try again.", "masterDataIssue_align_not_set": "Not set", - "masterDataIssue_align_warn_1to1": "BOM sales unit cannot be converted via M18 (likely a legacy Excel import). Sale qty was prefilled 1:1 — please verify before applying.", - "masterDataIssue_align_warn_bom_qty_null": "BOM qty not set on the left ({{fields}}). Adopt the M18 values on the right, or fix Excel and re-import.", - "masterDataIssue_align_warn_field_sale": "sale qty", + "masterDataIssue_align_warn_1to1": "This BOM sales unit cannot be converted with M18 (often an old Excel import). Sales quantity was filled in 1:1. Check it before applying.", + "masterDataIssue_align_warn_bom_qty_null": "Quantity is missing on the BOM ({{fields}}). Use the M18 values, or fix the Excel file and import it again.", + "masterDataIssue_align_warn_field_sale": "sales qty", "masterDataIssue_align_warn_field_stock": "stock qty", "masterDataIssue_align_warn_field_base": "base qty" } diff --git a/src/i18n/en/navigation.json b/src/i18n/en/navigation.json index be5b4659..eb0c45df 100644 --- a/src/i18n/en/navigation.json +++ b/src/i18n/en/navigation.json @@ -1,25 +1,25 @@ { - "menu": "menu", + "menu": "Menu", "nav.dashboard": "Dashboard", - "nav.storeManagement": "Store Management", - "nav.store.purchaseOrder": "Purchase Order", - "nav.store.pickOrder": "Pick Order", - "nav.store.inventoryLedger": "View item In-out And inventory Ledger", + "nav.storeManagement": "Warehouse Management", + "nav.store.purchaseOrder": "Purchase Orders", + "nav.store.pickOrder": "Pick Orders", + "nav.store.inventoryLedger": "Item In/Out and Inventory Ledger", "nav.store.itemTracing": "Item Tracing", - "nav.store.stockTake": "Stock Take Management", - "nav.store.stockIssue": "Stock Issue", - "nav.store.putAwayScan": "Put Away Scan", - "nav.store.finishedGoodManagement": "Finished Good Management", - "nav.store.stockRecord": "Stock Record", - "nav.store.doWorkbench": "DO Workbench", + "nav.store.stockTake": "Stock Take", + "nav.store.stockIssue": "Outbound Issues", + "nav.store.putAwayScan": "Put-away Scan", + "nav.store.finishedGoodManagement": "Finished Goods Management", + "nav.store.stockRecord": "Stock Records", + "nav.store.doWorkbench": "Finished Goods Outbound", "nav.deliveryOrder": "Delivery Order", - "nav.deliveryOrder.search": "Search Delivery Order", - "nav.deliveryOrder.replenish": "DO Replenishment", + "nav.deliveryOrder.search": "Search Delivery Orders", + "nav.deliveryOrder.replenish": "Delivery Order Replenishment", "nav.scheduling": "Scheduling", - "nav.jobOrderManagement": "Management Job Order", - "nav.jobOrder.searchCreate": "Search Job Order/ Create Job Order", - "nav.jobOrder.pickExecution": "Job Order Pick Execution", - "nav.jobOrder.productionProcess": "Job Order Production Process", + "nav.jobOrderManagement": "Job Orders", + "nav.jobOrder.searchCreate": "Search / Create Job Order", + "nav.jobOrder.pickExecution": "Job Order Picking", + "nav.jobOrder.productionProcess": "Production Process", "nav.jobOrder.bagUsage": "Bag Usage", "nav.bagPrint": "Bag Printer", "nav.laserPrint": "Laser Printer", @@ -33,25 +33,25 @@ "nav.chart.warehouse": "Inventory & Warehouse", "nav.chart.forecast": "Forecast & Planning", "nav.settings": "Settings", - "nav.settings.user": "User", + "nav.settings.user": "Users", "nav.settings.clientMonitor": "Device Connection Monitor", "nav.settings.items": "Items", - "nav.settings.itemDefaultShelfLife": "Item default shelf life", + "nav.settings.itemDefaultShelfLife": "Default Shelf Life", "nav.settings.equipment": "Equipment", "nav.settings.warehouse": "Warehouse", "nav.settings.printer": "Printer", "nav.settings.priceInquiry": "Price Inquiry", - "nav.settings.qcItem": "QC Check Item", - "nav.settings.qcCategory": "QC Category", - "nav.settings.qcItemAll": "QC Item All", - "nav.settings.shopAndTruck": "Shop And Truck", - "nav.settings.deliveryOrderFloor": "DO floor settings", - "nav.settings.demandForecast": "Demand Forecast Setting", - "nav.settings.bomWeighting": "BOM Weighting Score List", + "nav.settings.qcItem": "QC Inspection Items", + "nav.settings.qcCategory": "QC Templates", + "nav.settings.qcItemAll": "QC Management", + "nav.settings.shopAndTruck": "Shops and Truck Lanes", + "nav.settings.deliveryOrderFloor": "Delivery Order Floors", + "nav.settings.demandForecast": "Demand Forecast", + "nav.settings.bomWeighting": "BOM Weighting Scores", "nav.settings.masterDataIssues": "BOM / Item UOM Issues", "nav.settings.stockLedgerFix": "Stock Ledger Fix", "nav.breadcrumb.stockLedgerFix": "Stock Ledger Fix", - "nav.settings.qrCodeHandle": "QR Code Handle", + "nav.settings.qrCodeHandle": "QR Code Print and Download", "nav.settings.importTesting": "Import Testing", "nav.settings.importExcel": "Import Excel", "nav.settings.importBom": "Import BOM", @@ -65,34 +65,34 @@ "nav.breadcrumb.chartForecast": "Forecast & Planning", "nav.breadcrumb.projects": "Projects", "nav.breadcrumb.projectsCreate": "Create Project", - "nav.breadcrumb.tasks": "Task Template", + "nav.breadcrumb.tasks": "Task Templates", "nav.breadcrumb.tasksCreate": "Create Task Template", - "nav.breadcrumb.qcItem": "QC Item", - "nav.breadcrumb.qcItemAll": "QC Item All", - "nav.breadcrumb.qrCodeHandle": "QR Code Handle", - "nav.breadcrumb.demandForecast": "Demand Forecast Setting", - "nav.breadcrumb.deliveryOrderFloor": "Delivery Order Floor Settings", + "nav.breadcrumb.qcItem": "QC Items", + "nav.breadcrumb.qcItemAll": "QC Management", + "nav.breadcrumb.qrCodeHandle": "QR Code Print and Download", + "nav.breadcrumb.demandForecast": "Demand Forecast", + "nav.breadcrumb.deliveryOrderFloor": "Delivery Order Floors", "nav.breadcrumb.masterDataIssues": "BOM / Item UOM Issues", "nav.breadcrumb.equipment": "Equipment", - "nav.breadcrumb.equipmentMaintenanceEdit": "Maintenance Edit", - "nav.breadcrumb.shop": "Shop And Truck", + "nav.breadcrumb.equipmentMaintenanceEdit": "Edit Maintenance", + "nav.breadcrumb.shop": "Shops and Truck Lanes", "nav.breadcrumb.routeBoard": "Route Board", "nav.breadcrumb.shopDetail": "Shop Detail", "nav.breadcrumb.truckLaneDetail": "Truck Lane Detail", "nav.breadcrumb.printer": "Printer", "nav.breadcrumb.schedulingRough": "Demand Forecast", - "nav.breadcrumb.schedulingRoughEdit": "FG & Material Demand Forecast Detail", - "nav.breadcrumb.schedulingDetailed": "Detail Scheduling", - "nav.breadcrumb.schedulingDetailedEdit": "FG Production Schedule", + "nav.breadcrumb.schedulingRoughEdit": "Finished Goods and Material Forecast", + "nav.breadcrumb.schedulingDetailed": "Detailed Schedule", + "nav.breadcrumb.schedulingDetailedEdit": "Finished Goods Production Schedule", "nav.breadcrumb.inventory": "Inventory", "nav.breadcrumb.itemTracing": "Item Tracing", "nav.breadcrumb.importTesting": "Import Testing", "nav.breadcrumb.doWorkbenchSearch": "DO Workbench Search", "nav.breadcrumb.doWorkbenchPick": "DO Workbench Pick", "nav.breadcrumb.doWorkbenchEdit": "DO Workbench Detail", - "nav.breadcrumb.poEdit": "Edit", + "nav.breadcrumb.poEdit": "Edit Purchase Order", "nav.breadcrumb.poWorkbench": "PO Workbench", - "nav.breadcrumb.joEdit": "Edit Job Order", + "nav.breadcrumb.joEdit": "Job Order", "nav.breadcrumb.joTesting": "Job Order Testing", "nav.breadcrumb.joWorkbench": "Job Order Workbench", "nav.breadcrumb.report": "Report", @@ -100,7 +100,7 @@ "nav.breadcrumb.bagPrint": "Bag Printer", "nav.breadcrumb.laserPrint": "Laser Printer", "nav.breadcrumb.priceInquiry": "Price Inquiry", - "nav.breadcrumb.finishedGood": "Finished Good Order", - "nav.breadcrumb.finishedGoodManagement": "Finished Good Management", + "nav.breadcrumb.finishedGood": "Finished Goods Outbound", + "nav.breadcrumb.finishedGoodManagement": "Finished Goods Management", "nav.productionServer": "Production Server" } diff --git a/src/i18n/en/pickOrder.json b/src/i18n/en/pickOrder.json index 9bda0fff..195125e4 100644 --- a/src/i18n/en/pickOrder.json +++ b/src/i18n/en/pickOrder.json @@ -3,7 +3,7 @@ "Code": "Code", "Pick Order Code": "Pick Order Code", "Item Code": "Item Code", - "OrderDate": "OrderDate", + "OrderDate": "Order date", "Details": "Details", "Supplier": "Supplier", "Status": "Status", @@ -20,11 +20,11 @@ "Available Qty": "Available Qty", "Picked Qty": "Picked Qty", "Escalated": "Escalated", - "NotEscalated": "NotEscalated", + "NotEscalated": "Not escalated", "Assigned To": "Assigned To", "Progress": "Progress", "Select Remark": "Select Remark", - "Just Complete": "Just Complete", + "Just Complete": "Complete", "Skip": "Skip", "if need just edit number, please scan the lot again": "if need just edit number, please scan the lot again", "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}": "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}", @@ -34,12 +34,12 @@ "Required Date": "Required Date", "Store": "Store", "Available Orders": "Available Orders", - "This lot is rejected, please scan another lot.": "This lot is rejected, please scan another lot.", + "This lot is rejected, please scan another lot.": "This lot was rejected. Scan another lot.", "Lane Code": "Lane Code", - "Fetching all matching records...": "Fetching all matching records...", + "Fetching all matching records...": "Loading matching records…", "Edit": "Edit", "Submit Qty": "Submit Qty", - "Just Completed": "Just Completed", + "Just Completed": "Completed", "Just Completed (workbench): requires a valid lot number and quantity; expired rows must not use this button.": "Just Completed (workbench): requires a valid lot number and quantity; expired rows must not use this button.", "Just Completed (workbench): requires a valid lot number and quantity.": "Just Completed (workbench): requires a valid lot number and quantity.", "Do you want to start?": "Do you want to start?", @@ -47,8 +47,8 @@ "Pick Order Code(s)": "Pick Order Code(s)", "Delivery Order Code(s)": "Delivery Order Code(s)", "Start Success": "Start Success", - "Qty will submit": "Qty will submit", - "Truck Lance Code": "Truck Lance Code", + "Qty will submit": "Quantity to submit", + "Truck Lance Code": "Truck lane code", "Pick Order Codes": "Pick Order Codes", "Pick Order Lines": "Pick Order Lines", "Delivery Order Codes": "Delivery Order Codes", @@ -63,10 +63,10 @@ "Completed Date": "Completed Date", "Completed Time": "Completed Time", "Delivery Order": "Delivery Order", - "items": "items", + "items": "Items", "Select Pick Order:": "Select Pick Order:", "No Stock Available": "No Stock Available", - "is expired. Please check around have available QR code or not.": "is expired. Please check around have available QR code or not.", + "is expired. Please check around have available QR code or not.": "This lot is expired. Check nearby for an available QR code.", "Start Fail": "Start Fail", "Start PO": "Start PO", "Do you want to complete?": "Do you want to complete?", @@ -76,17 +76,17 @@ "Complete Pick Order": "Complete Pick Order", "General": "General", "Bind Storage": "Bind Storage", - "itemNo": "itemNo", - "itemName": "itemName", - "qty": "qty", + "itemNo": "Item code", + "itemName": "Item name", + "qty": "Qty", "Require Qty": "Require Qty", - "uom": "uom", + "uom": "UOM", "total weight": "total weight", "weight unit": "weight unit", "price": "price", "processed": "processed", - "expiryDate": "expiryDate", - "acceptedQty": "acceptedQty", + "expiryDate": "Expiry date", + "acceptedQty": "Received qty", "weight": "weight", "start": "start", "qc": "qc", @@ -94,9 +94,9 @@ "stock in": "stock in", "putaway": "putaway", "delete": "delete", - "qty cannot be greater than remaining qty": "qty cannot be greater than remaining qty", + "qty cannot be greater than remaining qty": "Quantity cannot be greater than the remaining quantity", "Record pol": "Record pol", - "Add some entries!": "Add some entries!", + "Add some entries!": "Nothing here yet.", "draft": "Draft", "pending": "Pending", "determine1": "Escalation 1", @@ -106,79 +106,79 @@ "received": "Awaiting Putaway", "completed": "Completed", "rejected": "Rejected", - "success": "success", - "acceptedQty must not greater than": "acceptedQty must not greater than", - "minimal value is 1": "minimal value is 1", - "value must be a number": "value must be a number", + "success": "Success", + "acceptedQty must not greater than": "Accepted quantity cannot be greater than", + "minimal value is 1": "Minimum value is 1", + "value must be a number": "Enter a number", "qc Check": "qc Check", - "Please select QC": "Please select QC", - "failQty": "failQty", - "select qc": "select qc", - "enter a failQty": "enter a failQty", + "Please select QC": "Please select a QC check", + "failQty": "Failed qty", + "select qc": "Select QC", + "enter a failQty": "Enter the failed quantity", "qty too big": "qty too big", - "sampleRate": "sampleRate", - "sampleWeight": "sampleWeight", - "totalWeight": "totalWeight", + "sampleRate": "Sample rate", + "sampleWeight": "Sample weight", + "totalWeight": "Total weight", "Escalation": "Escalation", "to be processed": "to be processed", "Stock In Detail": "Stock In Detail", - "productLotNo": "productLotNo", - "receiptDate": "receiptDate", - "acceptedWeight": "acceptedWeight", - "productionDate": "productionDate", - "reportQty": "reportQty", + "productLotNo": "Product lot no.", + "receiptDate": "Receipt date", + "acceptedWeight": "Accepted weight", + "productionDate": "Production date", + "reportQty": "Reported qty", "Default Warehouse": "Default Warehouse", "Select warehouse": "Select warehouse", - "Putaway Detail": "Putaway Detail", - "LotNo": "LotNo", - "Po Code": "Po Code", - "No Warehouse": "No Warehouse", - "Please scan warehouse qr code.": "Please scan warehouse qr code.", + "Putaway Detail": "Put-away details", + "LotNo": "Lot no.", + "Po Code": "PO code", + "No Warehouse": "No warehouse", + "Please scan warehouse qr code.": "Please scan the warehouse QR code.", "Reject": "Reject", - "submit": "submit", - "print": "print", - "bind": "bind", - "Total must equal Required Qty. Missing: {diff}": "Total must equal Required Qty. Missing: {diff}", - "Total must equal Required Qty. Exceeds by: {diff}": "Total must equal Required Qty. Exceeds by: {diff}", + "submit": "Submit", + "print": "Print", + "bind": "Bind", + "Total must equal Required Qty. Missing: {diff}": "The total must equal the required quantity. Short by: {diff}", + "Total must equal Required Qty. Exceeds by: {diff}": "The total must equal the required quantity. Over by: {diff}", "Batch": "Batch", "Single": "Single", "Release Type": "Release Type", - "isExtra order": "isExtra order", - "Etra": "Etra", - "Exit Etra view": "Exit Etra view", - "Etra Pick Order Detail": "Etra Pick Order Detail", - "Etra incomplete badge tooltip": "Incomplete extra tickets today: {{count}} (pending/released, excluding completed)", + "isExtra order": "Extra order", + "Etra": "Extra", + "Exit Etra view": "Exit extra view", + "Etra Pick Order Detail": "Extra pick order", + "Etra incomplete badge tooltip": "Incomplete extra tickets today: {{count}} (pending or released, not completed)", "Etra incomplete badge tooltip none": "No incomplete extra tickets", - "Back to normal assign tab": "Back to normal assign tab", - "Enter isExtra workbench view?": "Enter isExtra workbench view?", - "Etra view groups all add-on tickets by shop and lane for the selected date.": "Etra view groups all add-on tickets by shop and lane for the selected date.", - "Etra Ticket Notice": "Etra Ticket Notice", + "Back to normal assign tab": "Back to the normal assign tab", + "Enter isExtra workbench view?": "Open the extra view?", + "Etra view groups all add-on tickets by shop and lane for the selected date.": "The extra view groups extra tickets by shop and lane for the selected date.", + "Etra Ticket Notice": "Extra ticket notice", "batch": "Batch", "single": "Single", "isExtrabatch": "Extra batch", "isExtrasingle": "Extra single", - "isExtra": "Add-on", - "Merge Etra ticket": "Merge Etra ticket", - "Merge Etra ticket dialog title": "Merge Etra into batch ticket", - "Merge Etra ticket lane hint": "Only unassigned tickets on the same shop, floor (2/F, 4/F, or Truck X), truck lane, and departure time can be merged. Left: batch/single or TI-M merge ticket.", + "isExtra": "Extra", + "Merge Etra ticket": "Merge extra ticket", + "Merge Etra ticket dialog title": "Merge extra into batch ticket", + "Merge Etra ticket lane hint": "Only unassigned tickets with the same shop, floor (2/F, 4/F, or Truck X), truck lane, and departure time can be merged. The left side is a batch, single, or TI-M ticket.", "Merge left column title": "Batch / Single / Merge ticket", "Merge ticket TI-M": "TI-M merge ticket", "Batch/Single ticket": "Batch / Single ticket", - "Etra ticket": "Etra ticket", + "Etra ticket": "Extra ticket", "No mergeable batch tickets": "No unassigned batch or single tickets", - "No isExtra on same lane": "No isExtra tickets on the same lane", - "Select batch ticket first for isExtra": "Select a batch/single ticket on the left first", + "No isExtra on same lane": "No extra tickets on the same lane", + "Select batch ticket first for isExtra": "Select a batch or single ticket on the left first", "No delivery orders on ticket": "No delivery orders on this ticket", "Merge Etra ticket confirm": "Confirm merge", - "Merge Etra ticket confirm title": "Confirm merge batch and Etra tickets?", - "Merge Etra ticket confirm hint": "A new TI-M ticket will be created; the original batch and Etra tickets will be archived.", - "Merge Etra ticket confirm hint into TI-M": "The Etra ticket will be folded into the selected TI-M; the Etra ticket will be archived.", + "Merge Etra ticket confirm title": "Merge the batch and extra tickets?", + "Merge Etra ticket confirm hint": "A new TI-M ticket will be created. The original batch and extra tickets will be archived.", + "Merge Etra ticket confirm hint into TI-M": "The extra ticket will be merged into the selected TI-M ticket, then archived.", "Merge Etra ticket success": "Merge completed", "Merge Etra ticket failed": "Merge failed", "Shop search": "Shop search", "Confirm Search": "Search", "Merge Etra ticket search prompt": "Enter shop (optional) and date, then click Search to load merge candidates.", - "Merge Etra ticket search failed": "Failed to load merge candidates. Ensure the backend is updated and restarted.", + "Merge Etra ticket search failed": "Could not load tickets to merge. Try again.", "Pick Order": "Pick Order", "Type": "Type", "Product Type": "Product Type", @@ -199,18 +199,18 @@ "Target Date To": "Target Date To", "Consolidate": "Consolidate", "Stock Unit": "Stock Unit", - "create": "create", + "create": "Create", "detail": "detail", "Pick Order Detail": "Pick Order Detail", "item": "item", "Unit": "Unit", - "reset": "reset", - "targetDate": "targetDate", + "reset": "Reset", + "targetDate": "Target date", "remove": "remove", "release": "release", "location": "location", - "suggestedLotNo": "suggestedLotNo", - "lotNo": "lotNo", + "suggestedLotNo": "Suggested lot", + "lotNo": "Lot no.", "item name": "item name", "Item Name": "Item Name", "approval": "approval", @@ -248,26 +248,26 @@ "Group": "Group", "Qty Already Picked": "Qty Already Picked", "Select Job Order Items": "Select Job Order Items", - "failedQty": "failedQty", - "remarks": "remarks", + "failedQty": "Failed checks", + "remarks": "Remarks", "Qc items": "Qc items", - "qcItem": "qcItem", + "qcItem": "QC item", "QC Info": "QC Info", - "qcResult": "qcResult", - "acceptQty": "acceptQty", + "qcResult": "QC result", + "acceptQty": "Accepted qty", "Escalation History": "Escalation History", "Group Code": "Group Code", "Job Order Code": "Job Order Code", "QC Check": "QC Check", - "QR Code Scan": "QR Code Scan", + "QR Code Scan": "QR code scan", "Pick Order Details": "Pick Order Details", - "Partial quantity submitted. Please submit more or complete the order.": "Partial quantity submitted. Please submit more or complete the order.", - "Pick order completed successfully!": "Pick order completed successfully!", - "Lot has been rejected and marked as unavailable.": "Lot has been rejected and marked as unavailable.", - "This order is insufficient, please pick another lot.": "This order is insufficient, please pick another lot.", + "Partial quantity submitted. Please submit more or complete the order.": "Only part of the quantity was submitted. Submit the rest, or complete the order.", + "Pick order completed successfully!": "Pick order completed.", + "Lot has been rejected and marked as unavailable.": "This lot was rejected and marked unavailable.", + "This order is insufficient, please pick another lot.": "There is not enough quantity on this lot. Pick another lot.", "Please finish QR code scan, QC check and pick order.": "Please finish QR code scan, QC check and pick order.", - "No data available": "No data available", - "Please submit the pick order.": "Please submit the pick order.", + "No data available": "No data", + "Please submit the pick order.": "Submit the pick order.", "Item lot to be Pick:": "Item lot to be Pick:", "Report and Pick another lot": "Report and Pick another lot", "Accept Stock Out": "Accept Stock Out", @@ -275,7 +275,7 @@ "Delivery Note Code": "Delivery Note Code", "A4 Printer": "A4 Printer", "Label Printer": "Label Printer", - "Please select a printer first": "Please select a printer first", + "Please select a printer first": "Select a printer first", "Please select a label printer first": "Please select a label printer first", "Lot No": "Lot No", "Expiry Date": "Expiry Date", @@ -287,19 +287,19 @@ "Original Available Qty": "Original Available Qty", "Remaining Available Qty": "Remaining Available Qty", "Please submit pick order.": "Please submit pick order.", - "Please finish QR code scan and pick order.": "Please finish QR code scan and pick order.", + "Please finish QR code scan and pick order.": "Finish the QR code scan and the pick order.", "Please finish QR code scanand pick order.": "Please finish QR code scanand pick order.", - "First created group": "First created group", + "First created group": "First group created", "Latest created group": "Latest created group", "Manual Input": "Manual Input", - "QR Code Scan for Lot": "QR Code Scan for Lot", - "Processing QR code...": "Processing QR code...", - "The input is not the same as the expected lot number.": "The input is not the same as the expected lot number.", + "QR Code Scan for Lot": "QR code scan for lot", + "Processing QR code...": "Reading QR code…", + "The input is not the same as the expected lot number.": "The lot number does not match the expected lot.", "Verified successfully!": "Verified successfully!", "Cancel": "Cancel", "storing": "storing", "pick successful": "pick successful", - "Insufficient available quantity on lot (may have been picked by another user)": "Insufficient available quantity on lot (may have been picked by another user)", + "Insufficient available quantity on lot (may have been picked by another user)": "This lot does not have enough quantity left. Someone else may have picked it.", "Scan": "Scan", "Before today": "Before today", "Scanned": "Scanned", @@ -318,11 +318,11 @@ "Please assgin/release the pickorders to picker": "Please assgin/release the pickorders to picker", "Assign To": "Assign To", "No Group": "No Group", - "Selected items will join above created group": "Selected items will join above created group", + "Selected items will join above created group": "Selected items will be added to the group above.", "Issue": "Issue", "Pick Execution Issue Form": "Pick Execution Issue Form", "Lot line is unavailable": "Lot line is unavailable", - "This form is for reporting issues only. You must report either missing items or bad items.": "This form is for reporting issues only. You must report either missing items or bad items.", + "This form is for reporting issues only. You must report either missing items or bad items.": "Use this form to report a problem. Enter missing items or damaged items.", "Bad item Qty": "Bad item Qty", "Missing item Qty": "Missing item Qty", "Missing Item Qty": "Missing Item Qty", @@ -333,12 +333,12 @@ "Required Qty": "Required Qty", "Issue Remark": "Issue Remark", "Handler": "Handler", - "Qty is required": "Qty is required", - "Qty is not allowed to be greater than remaining available qty": "Qty is not allowed to be greater than remaining available qty", - "Qty is not allowed to be greater than required qty": "Qty is not allowed to be greater than required qty", - "At least one issue must be reported": "At least one issue must be reported", - "issueRemark": "issueRemark", - "handler": "handler", + "Qty is required": "Quantity is required", + "Qty is not allowed to be greater than remaining available qty": "Quantity cannot be greater than the remaining available quantity", + "Qty is not allowed to be greater than required qty": "Quantity cannot be greater than the required quantity", + "At least one issue must be reported": "Report at least one issue", + "issueRemark": "An issue description is required", + "handler": "Handler", "Max": "Max", "Route": "Route", "Index": "Index", @@ -362,15 +362,15 @@ "Stop QR Scan": "Stop QR Scan", "Scanning...": "Scanning...", "Print DN/Label": "Print DN/Label", - "Store ID": "Store ID", - "QR code does not match any item in current orders.": "QR code does not match any item in current orders.", + "Store ID": "Storage no.", + "QR code does not match any item in current orders.": "This QR code does not match any item on the current orders.", "Lot Number Mismatch": "Lot Number Mismatch", - "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?": "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?", + "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?": "This is the right item, but a different lot. Use this lot anyway?", "The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch.": "The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch.", "Expected Lot:": "Expected Lot:", "Scanned Lot:": "Scanned Lot:", "Confirm": "Confirm", - "Update your suggested lot to the this scanned lot": "Update your suggested lot to the this scanned lot", + "Update your suggested lot to the this scanned lot": "Update the suggested lot to this scanned lot.", "Print Draft": "Print Draft", "Print Pick Order and DN Label": "Print Pick Order and DN Label", "Print Pick Order": "Print Pick Order", @@ -394,14 +394,14 @@ "Number of cartons": "Number of cartons", "Select an action for the assigned pick orders.": "Select an action for the assigned pick orders.", "Detail": "Detail", - "consoCode": "consoCode", + "consoCode": "Consolidated code", "status": "status", "Items Included": "Items Included", "Pick Order Included": "Pick Order Included", - "No created items": "No created items", - "Please select item": "Please select item", - "enter a qty": "enter a qty", - "update qc info": "update qc info", + "No created items": "No items added yet", + "Please select item": "Select an item", + "enter a qty": "Enter a quantity", + "update qc info": "Update QC details", "All lots must be completed before printing": "All lots must be completed before printing", "Assigning pick order...": "Assigning pick order...", "Enter the number of cartons:": "Enter the number of cartons:", @@ -412,19 +412,19 @@ "Search date": "Search date", "Hide Completed: OFF": "Hide Completed: OFF", "Hide Completed: ON": "Hide Completed: ON", - "Number must be at least 1": "Number must be at least 1", - "Printed Successfully.": "Printed Successfully.", + "Number must be at least 1": "Enter a number of at least 1", + "Printed Successfully.": "Printed successfully.", "Product": "Product", - "You need to enter a number": "You need to enter a number", + "You need to enter a number": "Enter a number", "Available in warehouse": "Available in warehouse", "Describe the issue with bad items": "Describe the issue with bad items", "Enter pick qty or issue qty": "Enter pick qty or issue qty", "Invalid qty": "Invalid qty", "Note:": "Note:", - "Qty is not allowed to be greater than required/available qty": "Qty is not allowed to be greater than required/available qty", - "Still need to pick": "Still need to pick", + "Qty is not allowed to be greater than required/available qty": "Quantity cannot be greater than the required or available quantity", + "Still need to pick": "Still to pick", "Total exceeds required qty": "Total exceeds required qty", - "submitting": "submitting", + "submitting": "Submitting", "Back to List": "Back to List", "Delivery No": "Delivery No", "FG orders": "FG orders", @@ -432,32 +432,32 @@ "No Item": "No Item", "None": "None", "Add Selected Items to Created Items": "Add Selected Items to Created Items", - "All pick orders created successfully": "All pick orders created successfully", - "Failed to create group": "Failed to create group", + "All pick orders created successfully": "All pick orders were created.", + "Failed to create group": "Could not create the group", "Invalid date format": "Invalid date format", - "Item already exists in created items": "Item already exists in created items", - "Job Order not found or has no items": "Job Order not found or has no items", + "Item already exists in created items": "That item is already in the list", + "Job Order not found or has no items": "Job order not found, or it has no items.", "No results found": "No results found", - "Please enter at least code or name": "Please enter at least code or name", - "Please enter quantity for all selected items": "Please enter quantity for all selected items", - "Please select at least one item to submit": "Please select at least one item to submit", - "Please select group and enter quantity for all selected items": "Please select group and enter quantity for all selected items", - "Please select group for all selected items": "Please select group for all selected items", - "Please select product type": "Please select product type", - "Please select target date": "Please select target date", - "Please select type": "Please select type", + "Please enter at least code or name": "Enter a code or a name", + "Please enter quantity for all selected items": "Enter a quantity for every selected item", + "Please select at least one item to submit": "Select at least one item", + "Please select group and enter quantity for all selected items": "Choose a group and enter a quantity for every selected item", + "Please select group for all selected items": "Choose a group for every selected item", + "Please select product type": "Select a product type", + "Please select target date": "Select a target date", + "Please select type": "Select a type", "Search Criteria": "Search Criteria", "Processing...": "Processing...", "Failed items must have failed quantity": "Failed items must have failed quantity", "QC items without result": "QC items without result", - "confirm putaway": "confirm putaway", + "confirm putaway": "Confirm and put away", "email supplier": "email supplier", "qc processing": "qc processing", - "submitStockIn": "submitStockIn", - "not default warehosue": "not default warehosue", - "printQty": "printQty", + "submitStockIn": "Submit stock in", + "not default warehosue": "Not the default warehouse", + "printQty": "Print qty", "Shop": "Shop", - "warehouse": "warehouse", + "warehouse": "Warehouse", "Add Record": "Add Record", "Clean Record": "Clean Record", "Select": "Select", @@ -467,12 +467,12 @@ "Delivery Order Code": "Delivery Order Code", "Escalation Info": "Escalation Info", "Escalation Result": "Escalation Result", - "acceptQty must not greater than": "acceptQty must not greater than", - "supervisor": "supervisor", - "No Qc": "No Qc", - "receivedQty": "receivedQty", - "stock in information": "stock in information", - "No Uom": "No Uom", + "acceptQty must not greater than": "Accepted quantity cannot be greater than", + "supervisor": "Supervisor", + "No Qc": "No QC", + "receivedQty": "Received qty", + "stock in information": "Stock-in details", + "No Uom": "No UOM", "Input quantity cannot exceed": "Input quantity cannot exceed", "Quantity cannot be negative": "Quantity cannot be negative", "Enter bad item quantity (required if no missing items)": "Enter bad item quantity (required if no missing items)", @@ -494,15 +494,15 @@ "No entries available": "No entries available", "Today": "Today", "Tomorrow": "Tomorrow", - "packaging": "packaging", + "packaging": "Packaging", "This lot is not available, please scan another lot.": "This lot is not available, please scan another lot.", - "This lot UOM does not match the pick line. Please scan another lot.": "This lot UOM does not match the pick line. Please scan another lot.", - "Scanned lot UOM does not match pick line UOM": "This lot UOM does not match the pick line. Please scan another lot.", + "This lot UOM does not match the pick line. Please scan another lot.": "This lot's unit does not match the pick line. Scan another lot.", + "Scanned lot UOM does not match pick line UOM": "This lot's unit does not match the pick line. Scan another lot.", "This lot has already been picked. Please scan another lot.": "This lot has already been picked. Please scan another lot.", "Scanned lot is already completed or checked": "This lot has already been picked. Please scan another lot.", - "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.", + "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "Check nearby for a QR code. Stock may have just been received or transferred.", "Lot is expired (expiry={{expiry}})": "Lot is expired (expiry={{expiry}})", - "Day After Tomorrow": "Day After Tomorrow", + "Day After Tomorrow": "Day after tomorrow", "Select Date": "Select Date", "Suggest Lot No.": "Suggest Lot No.", "Search by Shop": "Search by Shop", @@ -530,27 +530,27 @@ "4F ticket": "4F ticket", "4F lane panel legend": "Lane — loading sequence (unassigned/total)", "Loading sequence n": "Board {{n}}", - "lot QR code": "Lot QR Code", - "label Printer": "label Printer", + "lot QR code": "Lot QR code", + "label Printer": "Label printer", "Loading Sequence": "Loading Sequence", "Ticket No": "Ticket No", - "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.", - "is unavable. Please check around have available QR code or not.": "This lot is unavailable. Please check around for an available QR code.", + "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "This lot is unavailable, so it was not switched or assigned. The pick line was not changed.", + "is unavable. Please check around have available QR code or not.": "This lot is unavailable. Check nearby for an available QR code.", "Lot switch failed; pick line was not marked as checked.": "Lot switch failed; pick line was not marked as checked.", "Lot confirmation failed. Please try again.": "Lot confirmation failed. Please try again.", "Powder Mixture": "Powder Mixture", - "No inventory lot lines for inventoryLotId": "This lot is not yet putaway", - "No inventory lot for stockInLineId": "This lot is not yet putaway", - "This lot is not yet putaway": "This lot is not yet putaway", + "No inventory lot lines for inventoryLotId": "This lot has not been put away yet", + "No inventory lot for stockInLineId": "This lot has not been put away yet", + "This lot is not yet putaway": "This lot has not been put away yet", "Cannot resolve new inventory lot line": "Cannot resolve new inventory lot line", "Pick order line item is null": "Pick order line item is null", - "New lot line item does not match pick order line item": "New lot line item does not match pick order line item", + "New lot line item does not match pick order line item": "This lot is a different item from the pick line.", "Pick order line {{id}} not found": "Pick order line {{id}} not found", - "SuggestedPickLot not found for pickOrderLineId {{polId}}": "SuggestedPickLot not found for pickOrderLineId {{polId}}", - "SuggestedPickLot qty is invalid: {{qty}}": "SuggestedPickLot qty is invalid: {{qty}}", + "SuggestedPickLot not found for pickOrderLineId {{polId}}": "No suggested pick lot was found for this pick line.", + "SuggestedPickLot qty is invalid: {{qty}}": "The suggested pick quantity is invalid: {{qty}}.", "Reject switch lot: available {{available}} less than required {{required}}": "Reject switch lot: available {{available}} less than required {{required}}", "Reject switch lot: picked {{picked}} already greater or equal required {{required}}": "Reject switch lot: picked {{picked}} already greater or equal required {{required}}", - "Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "Lot status is unavailable. Cannot switch or bind; pick line was not updated.", + "Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "This lot is unavailable, so it was not switched or assigned. The pick line was not changed.", "No lot rows. Select a line in the table above.": "No lot rows. Select a line in the table above.", "No stock out line for this lot": "No stock out line for this lot", "No data available for this pick order.": "No data available for this pick order.", @@ -564,8 +564,8 @@ "Truck Routing Summary": "Truck Routing Summary", "送貨路線摘要": "Truck Routing Summary", "車線-X": "Truck X", - "Lot QR Code": "Lot QR Code", - " 批號 QR 碼": "Lot QR Code", + "Lot QR Code": "Lot QR code", + " 批號 QR 碼": "Lot QR code", "Cannot determine this lot status": "Cannot determine this lot status", "Lot status: expired": "Lot status: expired", "This pick line was rejected. Please scan another lot.": "This pick line was rejected. Please scan another lot.", @@ -587,11 +587,11 @@ "Print copies": "Print copies", "Refresh lot list": "Refresh lot list", "Selected printer": "Selected: {{printer}}", - "Item code name": "Item: {{code}} {{name}}", + "Item code name": "Item: {{code}} {{name}}", "No available lots on this floor": "No available lots on this floor (available qty > 0).", " (current lot)": " (current lot)", "Location with value": "Location: {{location}}", - "Available qty with uom": "Available qty: {{qty}} UOM: {{uom}}", + "Available qty with uom": "Available qty: {{qty}} UOM: {{uom}}", "Print label": "Print label", "Show QR": "Show QR", "This row has no QR payload": "This row has no QR payload (stockInLineId required)", @@ -604,7 +604,7 @@ "Invalid scan format. Please scan again.": "Invalid scan format. Please scan again.", "Scan or look up once before refreshing the lot list.": "Scan or look up once before refreshing the lot list.", "Print quantity must be an integer of 1 or more": "Print quantity must be an integer of 1 or more", - "Print sent: Lot {{lotNo}}": "Print sent: Lot {{lotNo}}", + "Print sent: Lot {{lotNo}}": "Sent to printer: lot {{lotNo}}", "Print failed": "Print failed", "Failed to load FG carton quantity. Please try again later.": "Failed to load FG carton quantity. Please try again later.", "Cartons": "Cartons", @@ -625,7 +625,7 @@ "Exporting...": "Exporting...", "Download Excel": "Download Excel", "Download this view Excel": "Download Excel (Selected)", - "Download period Excel": "Download Excel (Complete)", + "Download period Excel": "Download Excel (full period)", "Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.", "Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.", "Filtered": "Filtered", diff --git a/src/i18n/en/po.json b/src/i18n/en/po.json index 20d110eb..51f8925c 100644 --- a/src/i18n/en/po.json +++ b/src/i18n/en/po.json @@ -1,8 +1,8 @@ { - "code": "code", + "code": "Code", "status": "status", - "escalated": "escalated", - "notEscalated": "notEscalated", + "escalated": "Escalated", + "notEscalated": "Not escalated", "All": "All", "Pending": "Pending", "pending": "Pending", @@ -12,8 +12,8 @@ "completed": "Completed", "Purchase Order": "Purchase Order", "Details": "Details", - "OrderDate": "OrderDate", + "OrderDate": "Order date", "Supplier": "Supplier", "Escalated": "Escalated", - "NotEscalated": "NotEscalated" + "NotEscalated": "Not escalated" } diff --git a/src/i18n/en/productionProcess.json b/src/i18n/en/productionProcess.json index efdff3e8..a7dd2668 100644 --- a/src/i18n/en/productionProcess.json +++ b/src/i18n/en/productionProcess.json @@ -1,10 +1,10 @@ { "Action": "Action", "All": "All", - "An error has occurred. Please try again later.": "An error has occurred. Please try again later.", + "An error has occurred. Please try again later.": "Something went wrong. Try again later.", "Are you sure you want to delete this process?": "Are you sure you want to delete this process?", "Assignment successful": "Assignment successful", - "Assume End Time": "Assume End Time", + "Assume End Time": "Expected end time", "Assume Time Need": "Assume Time Need", "Auto-refresh every 1 minute": "Auto-refresh every 1 minute", "Auto-refresh every 10 minutes": "Auto-refresh every 10 minutes", @@ -16,13 +16,13 @@ "Base Qty": "Base Qty", "Base UOM": "Base UOM", "Batch Count": "Batch Count", - "BoM Material": "BoM Material", - "Bom Req. Qty": "Bom Req. Qty", - "Bom Uom": "Bom Uom", + "BoM Material": "BOM material", + "Bom Req. Qty": "BOM required qty", + "Bom Uom": "BOM UOM", "By-product": "By-product", "Cancel": "Cancel", "Cancel Job Order": "Cancel Job Order", - "Cancel job order confirm message": "Cancel job order confirm message", + "Cancel job order confirm message": "Cancel this job order? It will be hidden from the list.", "Changeover Time": "Changeover Time", "Changeover Time (mins)": "Changeover Time (mins)", "Code": "Code", @@ -30,16 +30,16 @@ "Completed": "Completed", "Completed Step": "Completed Step", "Confirm": "Confirm", - "Confirm cancel job order": "Confirm cancel job order", - "Confirm delete job order": "Confirm delete job order", + "Confirm cancel job order": "Confirm cancellation", + "Confirm delete job order": "Confirm deletion", "Confirm to Pass this Process?": "Confirm to Pass this Process?", "Confirm to update this Job Order?": "Confirm to update this Job Order?", "Consumed Qty": "Consumed Qty", "Continue": "Continue", - "Count of Job Orders": "Count of Job Orders", + "Count of Job Orders": "Job orders", "Date": "Date", "Defect": "Defect", - "Delete job order confirm message": "Delete job order confirm message", + "Delete job order confirm message": "Delete this job order? This cannot be undone.", "Description": "Description", "Duration hours": "{{count}} hour(s)", "Duration minutes": "{{count}} minute(s)", @@ -50,7 +50,7 @@ "Equipment Code": "Equipment Code", "Equipment Name and Code": "Equipment Name and Code", "Equipment Type": "Equipment Type", - "EquipmentType-EquipmentName-Code": "EquipmentType-EquipmentName-Code", + "EquipmentType-EquipmentName-Code": "Equipment type – name – code", "Estimated Completion Time": "Estimated Completion Time", "Executing": "Executing", "FG / WIP Item": "FG / WIP Item", @@ -65,8 +65,8 @@ "pending": "Awaiting production", "stopped": "Stopped", "Invalid Job Order Id": "Invalid Job Order Id", - "Invalid Stock In Line Id": "Invalid Stock In Line Id", - "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity", + "Invalid Stock In Line Id": "Invalid stock-in line.", + "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "Dark | dense | float/sink | scrap rate | allergens | time sequence | complexity", "Item": "Item", "Item Code": "Item Code", "Item Name": "Item Name", @@ -128,14 +128,14 @@ "Handler": "Handler", "Goods Name": "Goods Name", "Job Type": "Job Type", - "Job process detail mode label": "Job process detail mode label", + "Job process detail mode label": "Process cell display", "Job process detail: equipment": "Equipment", "Job process detail: handler": "Handler", "Job process detail: process name": "Process Name", "Job process detail: time": "Time", "Just Pass": "Just Pass", "Last updated": "Last updated", - "Lines with insufficient stock: ": "Lines with insufficient stock: ", + "Lines with insufficient stock: ": "Items that could not be picked: ", "Lines with sufficient stock: ": "Lines with sufficient stock: ", "Lot No": "Lot No", "Matching Stock": "Matching Stock", @@ -143,7 +143,7 @@ "N/A": "N/A", "Name": "Name", "Next page": "Next page", - "No data available": "No data available", + "No data available": "No data", "No data found": "No data found", "No.": "No.", "Now": "Now", @@ -162,9 +162,9 @@ "Pending": "Pending", "Plan start (from)": "Plan start (from)", "Plan start (to)": "Plan start (to)", - "Please scan equipment code": "Please scan equipment code", - "Please scan operator code first": "Please scan operator code first", - "Please scan staff no": "Please scan staff no", + "Please scan equipment code": "Scan the equipment code", + "Please scan operator code first": "Scan the operator code first", + "Please scan staff no": "Scan the staff number", "Post Prod Time (Minutes)": "Post Prod Time (Minutes)", "Prep Time (Minutes)": "Prep Time (Minutes)", "Previous page": "Previous page", @@ -174,7 +174,7 @@ "Print Label": "Print Label", "Printing": "Printing", "Print job sent successfully": "Print job sent successfully", - "Please select a printer": "Please select a printer", + "Please select a printer": "Select a printer", "Please select a label printer": "Please select a label printer", "Process": "Process", "Process & Equipment": "Process & Equipment", @@ -197,7 +197,7 @@ "Production Process Steps": "Production Process Steps", "Production Time Remaining": "Production Time Remaining", "Production date": "Production date", - "Put Awayed Job Orders": "Put Awayed Job Orders", + "Put Awayed Job Orders": "Job orders put away", "Qty": "Qty", "Quality Check": "Quality Check", "Quantity": "Quantity", @@ -276,7 +276,7 @@ "Wait Time": "Wait Time", "Waiting QC Put Away Job Orders": "Waiting QC Put Away Job Orders", "Waiting QC Put Away": "Waiting QC Put Away", - "Put Awayed": "Put Awayed", + "Put Awayed": "Put away", "Put Away Detail": "Put Away Detail", "all": "All", "drink": "Drink", diff --git a/src/i18n/en/project.json b/src/i18n/en/project.json index 47f8b14e..d6dfb39f 100644 --- a/src/i18n/en/project.json +++ b/src/i18n/en/project.json @@ -8,9 +8,9 @@ "Project Code": "Project Code", "Project Code and Name": "Project Code and Name", "Release": "Release", - "View BoM": "View BoM", - "code": "code", + "View BoM": "View BOM", + "code": "Code", "Task": "Task", - "description": "description", + "description": "Description", "details": "details" } diff --git a/src/i18n/en/purchaseOrder.json b/src/i18n/en/purchaseOrder.json index f75266b5..23d8d91e 100644 --- a/src/i18n/en/purchaseOrder.json +++ b/src/i18n/en/purchaseOrder.json @@ -14,7 +14,7 @@ "Status": "Receipt Status", "escalateFrom": "Escalation Source", "Escalated": "Escalated", - "NotEscalated": "Not Escalated", + "NotEscalated": "Not escalated", "Do you want to start?": "Do you want to start?", "Start": "Start", "Start Success": "Start Success", @@ -28,7 +28,7 @@ "Complete Fail": "Complete Fail", "Complete PO": "Complete PO", "General": "General", - "Bind Storage": "Bind Storage", + "Bind Storage": "Assign location", "PO No.": "PO No.", "itemNo": "Item No.", "itemName": "Item Name", @@ -38,10 +38,10 @@ "Item Qty": "Item Qty", "Item": "Item", "Item Accepted Qty": "Item Accepted Qty", - "Item Purchase UoM": "Item Purchase UoM", + "Item Purchase UoM": "Purchase UOM", "qty": "Order Qty", - "uom": "Purchase UoM", - "Stock UoM": "Stock UoM", + "uom": "Purchase UOM", + "Stock UoM": "Stock UOM", "Stock In Qty": "Stock In Qty", "total weight": "Total Weight", "weight unit": "Weight Unit", @@ -54,7 +54,7 @@ "Confirm submit": "Confirm Submit", "This batch quantity exceeds order quantity. Do you still want to submit?": "This batch quantity exceeds order quantity. Do you still want to submit?", "qtyExceedsOrderConfirm": "Cumulative received quantity exceeds order quantity. Do you still want to submit?", - "Stock qty is not an integer": "Stock quantity is not an integer", + "Stock qty is not an integer": "The converted stock quantity is not a whole number", "Converted stock qty is": "Converted stock qty is", "Choose rounding method": ". Please choose a rounding method:", "Round ceiling": "Round up", @@ -79,10 +79,10 @@ "Accept quantity must be greater than 0": "Accept quantity must be greater than 0", "QC items without result": "QC items without result", "Failed items must have failed quantity": "Failed items must have failed quantity", - "qty cannot be greater than remaining qty": "Qty cannot be greater than remaining qty", - "acceptQty must not greater than": "Accept qty must not be greater than", + "qty cannot be greater than remaining qty": "Quantity cannot be greater than the remaining quantity", + "acceptQty must not greater than": "Accepted quantity cannot be greater than", "Record pol": "Record PO", - "Add some entries!": "Add some entries!", + "Add some entries!": "Nothing here yet.", "draft": "Draft", "pending": "Pending", "determine1": "Escalation 1", @@ -95,11 +95,11 @@ "rejected": "Rejected", "escalated": "Escalated", "status": "Status", - "acceptedQty must not greater than": "Accepted qty must not be greater than", + "acceptedQty must not greater than": "Accepted quantity cannot be greater than", "minimal value is 1": "Minimum value is 1", "value must be a number": "Value must be a number", "qc Check": "QC Check", - "Please select QC": "Please select QC", + "Please select QC": "Please select a QC check", "failQty": "Fail Qty", "select qc": "Select QC", "enter a failQty": "Enter fail qty", @@ -111,7 +111,7 @@ "to be processed": "To Be Processed", "supervisor": "Supervisor", "Stock In Detail": "Stock In Detail", - "productLotNo": "Delivery Lot No.", + "productLotNo": "Delivery lot no.", "receiptDate": "Receipt Date", "acceptedWeight": "Accepted Weight", "productionDate": "Production Date", @@ -121,9 +121,9 @@ "Putaway Detail": "Put Away Detail", "Delivery Detail": "Delivery Detail", "stockLotNo": "Stock Lot No.", - "Po Code": "PO No.", + "Po Code": "PO no.", "No Warehouse": "No Warehouse", - "Please scan warehouse qr code.": "Please scan warehouse QR code.", + "Please scan warehouse qr code.": "Please scan the warehouse QR code.", "receivedQty": "Received Qty", "dnQty": "Delivery Qty (This Batch)", "dnQtyOrderUnitHint": "in order unit", @@ -154,7 +154,7 @@ "remarks": "Remarks", "Reject": "Reject", "submit": "Submit", - "printQrCode": "Print QR Code", + "printQrCode": "Print QR code", "print": "Print", "bind": "Bind", "Search": "Search", @@ -179,7 +179,7 @@ "putawayUser": "Put Away User", "joCode": "Job Order No.", "salesUnit": "Sales Unit", - "download Qr Code": "Download QR Code", + "download Qr Code": "Download QR code", "downloading": "Downloading", "&": "&", "Calculate Expiry Date": "Calculate Expiry Date", @@ -197,7 +197,7 @@ "Add Record": "Add Record", "Clean Record": "Reset", "Create Material": "Create Material", - "Will start binding procedure after scanning item qr code.": "Will start binding procedure after scanning item qr code.", + "Will start binding procedure after scanning item qr code.": "Scanning the item QR code starts location assignment.", "Quantity": "Quantity", "Enter quantity": "Enter quantity", "Enter your remark": "Enter your remark" diff --git a/src/i18n/en/putAway.json b/src/i18n/en/putAway.json index 806ee3c3..2a3f0aa0 100644 --- a/src/i18n/en/putAway.json +++ b/src/i18n/en/putAway.json @@ -1,26 +1,26 @@ { "Pending scan": "Pending scan", - "Please scan warehouse qr code": "Please scan warehouse qr code", - "PoCode/JoCode": "PoCode/JoCode", + "Please scan warehouse qr code": "Please scan the warehouse QR code", + "PoCode/JoCode": "PO code / JO code", "Put Away": "Put Away", "Put Away Scan": "Put Away Scan", "Rescan": "Rescan", "Scanning": "Scanning", - "acceptedQty": "acceptedQty", - "confirm putaway": "confirm putaway", - "itemCode": "itemCode", - "itemName": "itemName", - "lotNo": "lotNo", - "minimal value is 1": "minimal value is 1", - "poCode": "poCode", - "putAwayHistory": "putAwayHistory", - "putQty": "putQty", - "putQty must not greater than": "putQty must not greater than", - "putawayQty": "putawayQty", - "scan loading": "scan loading", - "scan warehouse": "scan warehouse", - "uom": "uom", - "value must be a number": "value must be a number", - "value must be integer": "value must be integer", - "warehouse": "warehouse" + "acceptedQty": "Qty received this time", + "confirm putaway": "Confirm and put away", + "itemCode": "Item code", + "itemName": "Item name", + "lotNo": "Lot no.", + "minimal value is 1": "Minimum is 1", + "poCode": "PO code", + "putAwayHistory": "Put-away this time", + "putQty": "Qty to put away", + "putQty must not greater than": "Put-away quantity cannot be greater than", + "putawayQty": "Put-away qty", + "scan loading": "Loading, please wait…", + "scan warehouse": "Scan the warehouse QR code", + "uom": "UOM", + "value must be a number": "Enter a number", + "value must be integer": "Enter a whole number", + "warehouse": "Warehouse" } diff --git a/src/i18n/en/qcItemAll.json b/src/i18n/en/qcItemAll.json index 08e374c1..59c15469 100644 --- a/src/i18n/en/qcItemAll.json +++ b/src/i18n/en/qcItemAll.json @@ -2,7 +2,7 @@ "Code cannot be empty": "Code cannot be empty", "Code required!": "Code is required", "Error": "Error", - "Item {{itemCode}} is already mapped as {{type}} in category {{conflictCategory}}. Cannot set category {{currentCategory}} to {{newType}}.": "Item {{itemCode}} is already mapped as {{type}} in category {{conflictCategory}}. Cannot set category {{currentCategory}} to {{newType}}.", + "Item {{itemCode}} is already mapped as {{type}} in category {{conflictCategory}}. Cannot set category {{currentCategory}} to {{newType}}.": "Item {{itemCode}} is already {{type}} on template \"{{conflictCategory}}\". Template \"{{currentCategory}}\" cannot be set to {{newType}}.", "Loading...": "Loading...", "Name cannot be empty": "Name cannot be empty", "Name required!": "Name is required", @@ -10,18 +10,18 @@ "Add": "Add", "Add Association": "Add Association", "Add Mapping": "Add Mapping", - "Are you sure you want to delete this item?": "Are you sure you want to delete this item?", + "Are you sure you want to delete this item?": "Delete this item?", "Association Details": "Association Details", "Cancel": "Cancel", "Cannot Delete": "Cannot Delete", - "Cannot delete QcCategory linked": "Cannot delete QcCategory. It still has linked items or QC items.", - "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.": "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.", - "Cannot delete QcItem. It is linked to one or more QcCategories.": "Cannot delete QcItem. It is linked to one or more QcCategories.", + "Cannot delete QcCategory linked": "Cannot delete this QC template. Items or QC items are still linked to it.", + "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.": "Cannot delete this QC template. {itemCount} item(s) and {qcItemCount} QC item(s) are still linked to it.", + "Cannot delete QcItem. It is linked to one or more QcCategories.": "Cannot delete this QC item. It is still linked to one or more QC templates.", "Category": "Category", "Category Type": "Category Type", "Code": "Code", "Confirm": "Confirm", - "Confirm Delete": "Confirm Delete", + "Confirm Delete": "Confirm delete", "Create Qc Category": "Create Qc Category", "Create Qc Item": "Create Qc Item", "Delete": "Delete", @@ -29,7 +29,7 @@ "Delete Success": "Delete Success", "Description": "Description", "Details": "Details", - "Do you want to delete?": "Do you want to delete?", + "Do you want to delete?": "Delete this?", "Do you want to submit?": "Do you want to submit?", "Edit": "Edit", "Edit Qc Category": "Edit Qc Category", @@ -40,13 +40,13 @@ "Item Code": "Item Code", "Item Count": "Item Count", "Item Name": "Item Name", - "Item already has type \"{{type}}\" in QcCategory \"{{category}}\". One item can only have each type in one QcCategory.": "Item already has type \"{{type}}\" in QcCategory \"{{category}}\". One item can only have each type in one QcCategory.", + "Item already has type \"{{type}}\" in QcCategory \"{{category}}\". One item can only have each type in one QcCategory.": "This item is already type \"{{type}}\" on QC template \"{{category}}\". Each item can have a given type on only one template.", "Item and Qc Category Mapping": "Item and Qc Category Mapping", "Item code not found": "Item code not found", "Mapping Details": "Mapping Details", "Name": "Name", "No associations found": "No associations found", - "No data available": "No data available", + "No data available": "No data", "No mappings found": "No mappings found", "Order": "Order", "Qc Category": "Qc Category", @@ -63,8 +63,8 @@ "Qc Item Name": "Qc Item Name", "Save": "Save", "Select Item": "Select Item", - "Select Qc Category": "Select Qc Category", - "Select Qc Item": "Select Qc Item", + "Select Qc Category": "Select QC template", + "Select Qc Item": "Select QC item", "Select Type": "Select Type", "Submit": "Submit", "Submit Error": "Submit Error", diff --git a/src/i18n/en/qrCodeHandle.json b/src/i18n/en/qrCodeHandle.json index b5c8133c..54831988 100644 --- a/src/i18n/en/qrCodeHandle.json +++ b/src/i18n/en/qrCodeHandle.json @@ -1,5 +1,5 @@ { "PDF Preview": "PDF Preview", - "QR Code Handle": "QR Code Handle", + "QR Code Handle": "QR code print and download", "Equipment": "Equipment" } diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json index d58c6754..00e4c9a3 100644 --- a/src/i18n/en/report.json +++ b/src/i18n/en/report.json @@ -7,25 +7,25 @@ "searchCriteriaWithTitle": "Search Criteria: {{title}}", "downloadPdf": "Download Report (PDF)", "downloadExcel": "Download Report (Excel)", - "generatingPdf": "Generating PDF...", - "generatingExcel": "Generating Excel...", - "generatingReport": "Generating report...", - "generateError": "An error occurred while generating the report. Please try again.", + "generatingPdf": "Creating PDF…", + "generatingExcel": "Creating Excel…", + "generatingReport": "Creating the report…", + "generateError": "The report could not be generated. Try again.", "noDataFound": "No data found", - "noDataFoundTitle": "No Data Found / 查無資料", - "noDataFoundHint": "No inventory records match your search. Please try adjusting the filters.", + "noDataFoundTitle": "No data found", + "noDataFoundHint": "Nothing matched those filters. Try changing them.", "ok": "OK", - "unavailableInDemo": "Unavailable in demo mode", - "missingRequired": "Missing required fields:\n- {{fields}}", - "requiredField": "This is a required field", - "dateNotBeforeToday": "Date cannot be before today:\n- {{fields}}", - "selectOrEnterItemCode": "Select or enter item code", - "typeToSearchItemCode": "Enter at least {{min}} characters to search", + "unavailableInDemo": "Not available in the demo", + "missingRequired": "These fields are required:\n- {{fields}}", + "requiredField": "Required", + "dateNotBeforeToday": "This date cannot be before today:\n- {{fields}}", + "selectOrEnterItemCode": "Select or enter an item code", + "typeToSearchItemCode": "Type at least {{min}} characters to search", "noItemCodeMatches": "No matching item codes", - "searchingItemCodes": "Searching...", + "searchingItemCodes": "Searching…", "cancel": "Cancel", - "confirmDownloadPdf": "Confirm download PDF", - "confirmDownloadExcel": "Confirm download Excel", + "confirmDownloadPdf": "Download this PDF?", + "confirmDownloadExcel": "Download this Excel file?", "semiFgConfirmTitle": "Selected item codes — FG / Semi-FG Production Analysis Report", "semiFgConfirmHint": "Please confirm the selected item codes and their categories:", "semiFgColItem": "Item code and name", diff --git a/src/i18n/en/routeboard.json b/src/i18n/en/routeboard.json index d7622d59..f100b244 100644 --- a/src/i18n/en/routeboard.json +++ b/src/i18n/en/routeboard.json @@ -21,7 +21,7 @@ "Departure": "Departure", "Shops": "Shops", "Current version": "Current version", - "new arrangement": "new arrangement", + "new arrangement": "New arrangement", "Submitting...": "Submitting…", "saveChanges": "Save changes", "warnExpand": "Expand", @@ -46,7 +46,7 @@ "emDash": "—", "val_logisticsRequired": "Enter logistics company, plate, and driver name", "val_logisticsDuplicateName": "A logistics master or staged add with this name already exists", - "val_phoneInvalid": "Enter a valid phone number (digits)", + "val_phoneInvalid": "Enter a valid phone number (digits only)", "err_save": "Save failed", "err_invalidMasterId": "Invalid master record id", "err_exportNeedSelection": "Select at least one lane on the left to export", diff --git a/src/i18n/en/schedule.json b/src/i18n/en/schedule.json index 6d1ae646..05c84fbb 100644 --- a/src/i18n/en/schedule.json +++ b/src/i18n/en/schedule.json @@ -90,13 +90,13 @@ "Tue": "Tue", "Type": "Type", "Unselected": "Unselected", - "UoM": "UoM", - "View BoM": "View BoM", + "UoM": "UOM", + "View BoM": "View BOM", "View By FG": "View By FG", "View By Material": "View By Material", "Wed": "Wed", - "code": "code", - "consumables": "consumables", + "code": "Code", + "consumables": "Consumables", "detailed": "detailed", "fg": "fg", "item": "item", @@ -113,8 +113,8 @@ "Related Item Name": "Related Item Name", "Material Summary": "Material Summary", "mat": "mat", - "name": "name", - "non-consumables": "non-consumables", + "name": "Name", + "non-consumables": "Non-consumables", "sfg": "sfg", "type": "type", "Generate Job Order": "Generate Job Order" diff --git a/src/i18n/en/settings.json b/src/i18n/en/settings.json index 427d1c8a..5634471e 100644 --- a/src/i18n/en/settings.json +++ b/src/i18n/en/settings.json @@ -1,14 +1,14 @@ { - "settings": "settings", - "Demand Forecast Setting": "Demand Forecast Setting", - "Import Master Data": "Import Master Data", - "Import Po": "Import Po", - "QC Check Template": "QC Check Template", - "QC Check Template Created At": "QC Check Template Created At", - "QC Check Template Description": "QC Check Template Description", - "QC Check Template Details": "QC Check Template Details", - "QC Check Template Name": "QC Check Template Name", - "QC Check Template Status": "QC Check Template Status", - "QC Check Template Updated At": "QC Check Template Updated At", + "settings": "Settings", + "Demand Forecast Setting": "Demand forecast", + "Import Master Data": "Import master data", + "Import Po": "Import PO", + "QC Check Template": "QC template", + "QC Check Template Created At": "Template created", + "QC Check Template Description": "Template description", + "QC Check Template Details": "Template details", + "QC Check Template Name": "Template name", + "QC Check Template Status": "Template status", + "QC Check Template Updated At": "Template updated", "Ready to import": "Ready to import" } diff --git a/src/i18n/en/shop.json b/src/i18n/en/shop.json index d0fdfb34..93c915f1 100644 --- a/src/i18n/en/shop.json +++ b/src/i18n/en/shop.json @@ -49,7 +49,7 @@ "Name": "Name", "No Truck Lane data available": "No Truck Lane data available", "No Truck data available": "No Truck data available", - "No TruckLance": "No TruckLance", + "No TruckLance": "No truck lane", "No changes": "No changes", "No shops found using this truck lane": "No shops found using this truck lane", "No truck lane data available": "No truck lane data available", @@ -76,7 +76,7 @@ "ShopAndTruck": "Lane & shop management", "Shops": "Shops", "Shops Using This Truck Lane": "Shops Using This Truck Lane", - "Store ID": "Store ID", + "Store ID": "Floor", "Submitting...": "Submitting…", "Truck Information": "Truck Information", "Truck Lane": "Truck Lane", @@ -86,9 +86,9 @@ "Truck lane information is required": "Truck lane information is required", "Truck lane not found": "Truck lane not found", "Truck shop details updated successfully": "Truck shop details updated successfully", - "TruckLance Code": "TruckLance Code", - "TruckLance Code is required": "TruckLance Code is required", - "TruckLance Status": "TruckLance Status", + "TruckLance Code": "Truck lane code", + "TruckLance Code is required": "Truck lane code is required", + "TruckLance Status": "Truck lane status", "View Detail": "View Detail", "addRoute_confirm": "Confirm add lane", "addRoute_dialogTitle": "Add delivery lane", @@ -121,7 +121,7 @@ "btn_scheduleChange": "Schedule changes", "btn_scheduleHistory": "Schedule change log", "cancel": "Cancel", - "code": "code", + "code": "Code", "confirm_addShopConflict": "Detected {{count}} potential conflict(s) with other lanes (Rules 1/2; see bell). It will be added to the board first; press \"Save changes\" to persist. Continue?", "confirm_clearLane": "Clear all {{count}} shop(s) from lane \"{{laneLabel}}\"? (Press \"Save changes\" to delete on server)", "confirm_deleteLogistic": "Delete logistics company \"{{name}}\"? Press \"Save changes\" to persist.", @@ -252,7 +252,7 @@ "mtmsRouteWarn_tooltipHas": "{{count}} potential conflict(s)", "mtmsRouteWarn_tooltipNone": "No alerts", "nav_unsavedLeave": "You have unsaved changes. Leave this page?", - "new arrangement": "new arrangement", + "new arrangement": "New arrangement", "pageTitle": "MTMS route & shop board", "quickIndex": "Quick index", "quickPick_noKeyword": "No lanes match the keyword", @@ -433,7 +433,7 @@ "tooltip_removeFromLane": "Remove from this lane", "val_logisticsDuplicateName": "A logistics master or staged add with this name already exists", "val_logisticsRequired": "Enter logistics company, plate, and driver name", - "val_phoneInvalid": "Enter a valid phone number (digits)", + "val_phoneInvalid": "Enter a valid phone number (digits only)", "versionLogDialogTitle": "Version change log", "versionNote_saveFail": "Failed to save note", "version_date_label": "Date", diff --git a/src/i18n/en/stockIssue.json b/src/i18n/en/stockIssue.json index 45590bce..7dee12c3 100644 --- a/src/i18n/en/stockIssue.json +++ b/src/i18n/en/stockIssue.json @@ -49,7 +49,7 @@ "Item not found": "Item not found", "Item selected": "Item selected", "JO Order Code": "JO Order Code", - "Loading": "Loading", + "Loading": "Loading…", "Location": "Location", "Looked": "Looked", "Lot No": "Lot No", @@ -62,7 +62,7 @@ "No record found": "No record found", "Picker Name": "Picker Name", "Pick Order Code": "Pick Order Code", - "Please enter a valid quantity": "Please enter a valid quantity", + "Please enter a valid quantity": "Enter a valid quantity", "Please set at least one search criterion": "Please set at least one search criterion", "Processing...": "Processing...", "Quantity exceeds available quantity": "Quantity exceeds available quantity", @@ -78,15 +78,15 @@ "Search to load lot lines": "Search to load lot lines", "Start Date": "Start Date", "Status": "Status", - "Stock UoM": "Stock UoM", + "Stock UoM": "Stock UOM", "Submit": "Submit", "Submit Bad Item": "Submit Bad Item", - "Submit Miss Item": "Submit Miss Item", + "Submit Miss Item": "Submit shortage", "Submit Quantity": "Submit Quantity", "Submitting...": "Submitting...", "Type": "Type", "Unknown error": "Unknown error", - "UoM": "UoM", + "UoM": "Stock UOM", "User ID is required": "User ID is required", "Warehouse": "Warehouse", "available": "Available", diff --git a/src/i18n/en/stockLedgerFix.json b/src/i18n/en/stockLedgerFix.json index 7ec6dc0d..510cee21 100644 --- a/src/i18n/en/stockLedgerFix.json +++ b/src/i18n/en/stockLedgerFix.json @@ -39,7 +39,7 @@ "tabCalendar": "Calendar", "tabInventory": "Inventory", "tabLot": "Lot line", - "calendarHint": "Click the title at the top-left to pick year, then month.\nChanging month does not load data; click a day to inspect that day.\nToday can be fixed (freeze-dump night).", + "calendarHint": "Click the title at the top left to pick the year, then the month.\nChanging the month does not load data. Click a day to inspect it.\nToday can be repaired (nightly freeze).", "selectDate": "Select a date", "dayLoadError": "Could not load the day’s check", "dayLedgerCount": "{{cnt}} ledger rows that day", @@ -96,17 +96,17 @@ "searchFailed": "Search failed", "colInventoryId": "inventoryId", "colItemCode": "itemCode", - "colUomId": "uomId", + "colUomId": "UOM id", "colLedgerRows": "ledger rows", "invLoadError": "Could not load inventory", "invScopeSummary": "{{itemCode}} / inventory {{id}} / uom {{uomId}}; {{firstDate}} → {{lastDate}}; {{cnt}} rows; last balance {{lastBalance}}", "invFixConfirm": "This will recalculate lotQty and day close for all ledger rows of this inventory up to today, plus this item’s balance. Continue?", "invFixDone": "Fixed inventory {{id}}: lot {{lot}}, uom {{uom}}, inventory {{inventory}}, lotQty {{lotQty}}, balance {{balance}}, day close {{dayRows}}", "fixThisInventory": "Fix this inventory", - "lotTabHint": "Search by lotNo or inventoryLotLineId. Fix only recalculates this lot’s lotQty and day close; it does not change balance.", - "lotSearchLabel": "lotNo / lot line id", + "lotTabHint": "Search by lot number or lot line id. Repair recalculates this lot's quantity and day close. It does not change the balance.", + "lotSearchLabel": "Lot no. / lot line id", "colLotLineId": "lot line id", - "colLotNo": "lotNo", + "colLotNo": "Lot no.", "lotLoadError": "Could not load lot", "lotScopeSummary": "{{lotNo}} / lot {{id}} / {{itemCode}}; {{firstDate}} → {{lastDate}}; {{cnt}} rows; last lotQtyAfter {{lastLotQtyAfter}}", "lotFixConfirm": "This only fixes this lot’s lotQty and day close; it will not change the item’s balance. Continue?", diff --git a/src/i18n/en/stockRecord.json b/src/i18n/en/stockRecord.json index 5b0ca0d5..26d8a8dd 100644 --- a/src/i18n/en/stockRecord.json +++ b/src/i18n/en/stockRecord.json @@ -1,16 +1,16 @@ { "title": "Stock Record", - "tke": "tke", - "adj": "adj", - "nor": "nor", - "trf": "trf", - "open": "open", - "miss": "miss", - "bad": "bad", + "tke": "Stock take", + "adj": "Adjustment", + "nor": "Normal", + "trf": "Transfer", + "open": "Opening", + "miss": "Shortage", + "bad": "Damaged", "Start Date": "Start Date", "End Date": "End Date", "Date": "Date", - "Item-lotNo": "Item-lotNo", + "Item-lotNo": "Item / lot no.", "UOM": "UOM", "In Qty": "In Qty", "Out Qty": "Out Qty", diff --git a/src/i18n/en/stockTake.json b/src/i18n/en/stockTake.json index 64f04baf..be61333b 100644 --- a/src/i18n/en/stockTake.json +++ b/src/i18n/en/stockTake.json @@ -13,16 +13,16 @@ "Approver Input": "Approver Input", "Approver Pending": "Approver Pending", "Approver Time": "Approver Time", - "Approver input empty; save skipped, row remains pending": "Approver input empty; save skipped, row remains pending", + "Approver input empty; save skipped, row remains pending": "Nothing was entered, so this row was not saved and stays pending.", "Approver stock take record saved successfully": "Approver stock take record saved successfully", "Area": "Area", - "Available QTY cannot be negative": "Available QTY cannot be negative", + "Available QTY cannot be negative": "Available quantity cannot be negative", "Back to List": "Back to List", "Bad Qty": "Bad Qty", "Batch Save All": "Batch Save All", "Batch approver save completed: {{success}} success, {{errors}} errors": "Batch approver save completed: {{success}} success, {{errors}} errors", "Batch save completed: {{success}} success, {{errors}} errors": "Batch save completed: {{success}} success, {{errors}} errors", - "Batch save confirm message": "Batch save {{count}} stock take record(s) in the current list. Continue?", + "Batch save confirm message": "Save {{count}} stock-take record(s) in the current list?", "Before Qty": "Before Qty", "Book Qty": "Book Qty", "Cancel": "Cancel", @@ -33,12 +33,12 @@ "Confirm batch save approver": "Confirm batch save", "Confirm create stock take": "Confirm create stock take", "Control Time": "Control Time", - "Create Stock Take (Select Sections)": "Create stock take (select sections)", + "Create Stock Take (Select Sections)": "Create a stock take (select sections)", "Created": "Created", "Creation date": "Creation date", "Damage": "Damage", "Date": "Date", - "Deselect all on this floor": "Deselect all on this floor ({{floor}})", + "Deselect all on this floor": "Clear this floor ({{floor}})", "Detail": "Detail", "Difference": "Difference", "Errors": "Errors", @@ -60,7 +60,7 @@ "Issue Remark": "Issue Remark", "Item": "Item", "Item Code": "Item Code", - "Item-lotNo-ExpiryDate": "Item-lotNo-ExpiryDate", + "Item-lotNo-ExpiryDate": "Item / lot no. / expiry date", "Last Stock Take Date": "Last Stock Take Date", "Location": "Location", "Lot No": "Lot No", @@ -74,7 +74,7 @@ "No stock take sections from warehouse": "No stock take sections returned from warehouse.", "No valid input to submit": "No valid input to submit", "No valid records to batch save": "No valid records to batch save", - "Not Match": "Not Match", + "Not Match": "Recount required", "Not filled": "(not filled)", "Only Variance": "Only Variance", "Pass": "Pass", @@ -88,7 +88,7 @@ "Please enter Approver QTY": "Please enter approver count quantity", "Please enter QTY": "Please enter quantity", "Please enter Second QTY": "Please enter second count quantity", - "ReStockTake": "ReStockTake", + "ReStockTake": "Recount", "Reason": "Reason", "Record Status": "Record Status", "Rejected": "Rejected", @@ -124,11 +124,11 @@ "Stock take qty exceeds maximum": "Stock take quantity cannot exceed 999,999,999,999", "Stock take record ID is required": "Stock take record ID is required", "Stock take record saved successfully": "Stock take record saved successfully", - "Stock take record status updated to not match": "Stock take record status updated to not match", + "Stock take record status updated to not match": "Stock take status set to recount required", "Stock take round name": "Stock take round name", "Stock take round name placeholder": "Optional, e.g. May full-site stock take", "Stock take sections in current list": "{{count}} stock take section(s) in current list", - "Store ID": "Store ID", + "Store ID": "Floor", "Submit All Inputted": "Submit All Inputted", "Total": "Total", "Total Adjustments": "Total Adjustments", @@ -145,7 +145,7 @@ "Variance filter inclusive only": "Show only rows within variance range", "Variance filter inclusive range hint": "Show rows with -{{value}}% {{op}} variance % {{op}} {{value}}% (within range)", "View Details": "View Details", - "View ReStockTake": "View ReStockTake", + "View ReStockTake": "View recount", "Warehouse": "Warehouse", "Warehouse Location": "Warehouse Location", "Warehouse missing stock take section drawer hint": "These locations have no stock take section (ST-xxx) and cannot be included in stock take. Fix in warehouse settings.", @@ -156,23 +156,23 @@ "Warehouse missing stock take section tooltip none": "All warehouses have a stock take section", "Warehouse missing stock take section warn title": "Warehouses without stock take section", "approving": "Approving", - "book qty": "book qty", + "book qty": "Book quantity", "completed": "Completed", "do": "Delivery order", - "end time": "end time", + "end time": "End time", "jo": "Job order", "lot_issue": "Lot issue", "material": "Material", - "notMatch": "Re-count required", - "notmatch": "Re-count required", + "notMatch": "Recount required", + "notmatch": "Recount required", "pass": "Counted", "pending": "Pending", "quality_issue": "Quality issue", "quantity_mismatch": "Quantity mismatch", "resolved": "Resolved", "sections unit": "area(s)", - "selected stock take qty": "selected stock take qty", - "start time": "start time", + "selected stock take qty": "Selected count quantity", + "start time": "Start time", "qtyGapWarnPercent": "Qty variance warning %", "qtyGapWarnPercentSaved": "Qty gap warning percent saved", "qtyGapWarnPercentInvalid": "Enter a whole number from 0 to 1000", @@ -180,5 +180,5 @@ "stockTakeQtyGapWarnOver": "Please check the entry. The count differs from current stock by more than {{threshold}}%.", "stockTaking": "Stock taking", "stock_take": "Stock take", - "variance Percentage": "variance Percentage" + "variance Percentage": "Variance %" } diff --git a/src/i18n/en/ticketReleaseTable.json b/src/i18n/en/ticketReleaseTable.json index bb0a9732..5d805e34 100644 --- a/src/i18n/en/ticketReleaseTable.json +++ b/src/i18n/en/ticketReleaseTable.json @@ -12,7 +12,7 @@ "Confirm": "Confirm", "Confirm force complete": "Confirm force complete", "Confirm revert assignment": "Confirm revert assignment", - "Day After Tomorrow": "Day After Tomorrow", + "Day After Tomorrow": "Day after tomorrow", "Departure Time": "Departure Time", "Floor": "Floor", "Force complete DO": "Force complete DO", @@ -21,7 +21,7 @@ "Last updated": "Last updated", "Loading Sequence": "Loading Sequence", "Manager only hint": "Admin only", - "No data available": "No data available", + "No data available": "No data", "Now": "Now", "Number of FG Items (Order Item(s) Count)": "Number of FG Items (Order Item(s) Count)", "Operation succeeded": "Operation succeeded", @@ -36,7 +36,7 @@ "Shop Name": "Shop Name", "Status": "Status", "Unassigned": "Unassigned", - "Store ID": "Store ID", + "Store ID": "Floor", "Target Date": "Target Date", "Ticket Information": "Ticket Information", "Ticket No.": "Ticket No.", diff --git a/src/i18n/en/user.json b/src/i18n/en/user.json index 4ebea06d..761316f3 100644 --- a/src/i18n/en/user.json +++ b/src/i18n/en/user.json @@ -1,26 +1,26 @@ { - "profile": "profile", + "profile": "Profile", "Add": "Add", - "Allocated Authority": "Allocated Authority", + "Allocated Authority": "Assigned permissions", "Approval": "Approval", "Authority": "Authority", - "Authority Pool": "Authority Pool", + "Authority Pool": "Available permissions", "Cancel": "Cancel", "Confirm": "Confirm", "Confirm Password": "Confirm Password", "Create User": "Create User", "Delete": "Delete", - "Delete Success": "Delete Success", - "Do you want to delete?": "Do you want to delete?", + "Delete Success": "Deleted", + "Do you want to delete?": "Delete this?", "Edit": "Edit", "Edit User": "Edit User", - "Failed to fetch staff list": "Failed to fetch staff list", - "Maintain User": "Maintain User", + "Failed to fetch staff list": "Could not load the staff list", + "Maintain User": "Maintain user", "Maintain group": "Maintain group", "Remove": "Remove", "Reset": "Reset", "Rows per page": "Rows per page", - "Search by Authority or description or position.": "Search by Authority or description or position.", + "Search by Authority or description or position.": "Search by authority, description, or position.", "Testing": "Testing", "User": "User", "User Authority": "User Authority", @@ -29,24 +29,24 @@ "User ID": "User ID", "User Name": "User Name", "Username": "Username", - "authority": "authority", - "description": "description", - "name": "name", - "password": "password", - "qrcode": "qrcode", - "staffNo": "staffNo", - "user": "user", - "username": "username", - "view group": "view group", - "view user": "view user", - "An error has occurred. Please try again later.": "An error has occurred. Please try again later.", - "Please input correct password": "Please input correct password", - "The password requires 8-20 characters.": "The password requires 8-20 characters.", + "authority": "Authority", + "description": "Description", + "name": "Name", + "password": "Password", + "qrcode": "QR code", + "staffNo": "Staff no.", + "user": "User", + "username": "Username", + "view group": "View group", + "view user": "View user", + "An error has occurred. Please try again later.": "Something went wrong. Try again later.", + "Please input correct password": "Please enter the correct password.", + "The password requires 8-20 characters.": "Use 8 to 20 characters.", "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.": "The password must be 8-20 characters and include uppercase letters, lowercase letters, numbers, and symbols.", - "Failed to search by name": "Failed to search by name", - "Failed to search by username": "Failed to search by username", - "Staff No is required": "Staff No is required", - "User Not Found": "User Not Found", + "Failed to search by name": "Could not search by name", + "Failed to search by username": "Could not search by username", + "Staff No is required": "Staff number is required", + "User Not Found": "User not found", "Username is already taken": "Username is already taken. Please choose another.", "Name is already taken": "Name is already taken. Please choose another.", "Staff No is already taken": "Staff No is already taken. Please choose another.", diff --git a/src/i18n/en/warehouse.json b/src/i18n/en/warehouse.json index 1aef4227..111ace41 100644 --- a/src/i18n/en/warehouse.json +++ b/src/i18n/en/warehouse.json @@ -8,7 +8,7 @@ "Create Warehouse": "Create Warehouse", "Delete": "Delete", "Delete Success": "Delete Success", - "Do you want to delete?": "Do you want to delete?", + "Do you want to delete?": "Delete this?", "Edit": "Edit", "Edit Warehouse": "Edit Warehouse", "Mapping Details": "Mapping Details", @@ -24,7 +24,7 @@ "Stock Take Section": "Stock Take Section", "Stock Take Section & Warehouse Mapping": "Stock Take Section & Warehouse Mapping", "Stock Take Section Description": "Stock Take Section Description", - "Store ID": "Store ID", + "Store ID": "Floor", "Warehouse": "Warehouse", "Warehouse Detail": "Warehouse Detail", "Warehouse List": "Warehouse List", @@ -33,12 +33,12 @@ "capacity": "Capacity", "code": "Code", "description": "Description", - "is required": "is required", + "is required": "Required", "name": "Name", "order": "Order", "slot": "Slot", "stockTakeSection": "Stock Take Section", - "stockTakeSectionDescription": "stockTakeSectionDescription", + "stockTakeSectionDescription": "Stock-take section description", "store_id": "Store ID", - "warehouse": "warehouse" + "warehouse": "Warehouse" } diff --git a/src/i18n/installAcceptLanguage.ts b/src/i18n/installAcceptLanguage.ts new file mode 100644 index 00000000..be266811 --- /dev/null +++ b/src/i18n/installAcceptLanguage.ts @@ -0,0 +1,19 @@ +"use client"; + +import { clientAcceptLanguage } from "@/i18n/locale"; + +const FLAG = "__fpsmsAcceptLanguage"; + +if (typeof window !== "undefined" && !(window as unknown as Record)[FLAG]) { + (window as unknown as Record)[FLAG] = true; + const original = window.fetch.bind(window); + window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { + const headers = new Headers( + init?.headers ?? (input instanceof Request ? input.headers : undefined), + ); + if (!headers.has("Accept-Language")) { + headers.set("Accept-Language", clientAcceptLanguage()); + } + return original(input, { ...init, headers }); + }; +} diff --git a/src/i18n/locale.ts b/src/i18n/locale.ts index cd846963..8d252022 100644 --- a/src/i18n/locale.ts +++ b/src/i18n/locale.ts @@ -21,3 +21,15 @@ export function setLanguageCookie(lang: AppLanguage) { if (typeof document === "undefined") return; document.cookie = `${I18N_COOKIE_NAME}=${lang}; Path=/; SameSite=Lax; Max-Age=31536000`; } + +/** Language sent to the API. Cookie wins, then the document language, then Chinese. */ +export function clientAcceptLanguage(): AppLanguage { + if (typeof document === "undefined") return FALLBACK_LANG; + const raw = document.cookie + .split("; ") + .find((part) => part.startsWith(`${I18N_COOKIE_NAME}=`)) + ?.slice(I18N_COOKIE_NAME.length + 1); + const fromCookie = normalizeAppLanguage(raw ? decodeURIComponent(raw) : null); + if (fromCookie) return fromCookie; + return normalizeAppLanguage(document.documentElement?.lang) ?? FALLBACK_LANG; +} diff --git a/src/i18n/useUiText.ts b/src/i18n/useUiText.ts new file mode 100644 index 00000000..19215314 --- /dev/null +++ b/src/i18n/useUiText.ts @@ -0,0 +1,16 @@ +"use client"; + +import { useTranslation } from "react-i18next"; + +/** English UI copy when the app language is English. Chinese stays the default. */ +export function useUiText() { + const { i18n } = useTranslation(); + const english = (i18n.resolvedLanguage || i18n.language || "") + .toLowerCase() + .startsWith("en"); + return (zh: string, en: string) => (english ? en : zh); +} + +export function isEnglishLanguage(language: string | undefined | null) { + return (language || "").toLowerCase().startsWith("en"); +} diff --git a/src/theme/ThemeRegistry.tsx b/src/theme/ThemeRegistry.tsx index 22c9831c..613c5805 100644 --- a/src/theme/ThemeRegistry.tsx +++ b/src/theme/ThemeRegistry.tsx @@ -1,4 +1,5 @@ "use client"; +import "@/i18n/installAcceptLanguage"; import * as React from "react"; import { ThemeProvider, createTheme } from "@mui/material/styles"; import CssBaseline from "@mui/material/CssBaseline";