add en/vi language

This commit is contained in:
Tran Anh Tuan
2025-11-15 16:58:07 +07:00
parent 1a534eccb0
commit e725819c01
31 changed files with 1843 additions and 232 deletions

View File

@@ -2,6 +2,7 @@ import Select from "@/components/Select";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { queryGpsData } from "@/controller/DeviceController";
import { queryUpdateFishingLogs } from "@/controller/TripController";
import { useI18n } from "@/hooks/use-i18n";
import { showErrorToast, showSuccessToast } from "@/services/toast_service";
import { useFishes } from "@/state/use-fish";
import { useTrip } from "@/state/use-trip";
@@ -46,21 +47,16 @@ const SIZE_UNITS_OPTIONS = SIZE_UNITS.map((unit) => ({
// Zod schema cho 1 dòng cá
const fishItemSchema = z.object({
id: z.number().min(1, "Chọn loài cá"),
quantity: z
.number({ invalid_type_error: "Số lượng phải là số" })
.positive("Số lượng > 0"),
unit: z.enum(UNITS, { required_error: "Chọn đơn vị" }),
size: z
.number({ invalid_type_error: "Kích thước phải là số" })
.positive("Kích thước > 0")
.optional(),
id: z.number().min(1, ""),
quantity: z.number({ invalid_type_error: "" }).positive(""),
unit: z.enum(UNITS, { required_error: "" }),
size: z.number({ invalid_type_error: "" }).positive("").optional(),
sizeUnit: z.enum(SIZE_UNITS),
});
// Schema tổng: mảng các item
const formSchema = z.object({
fish: z.array(fishItemSchema).min(1, "Thêm ít nhất 1 loài cá"),
fish: z.array(fishItemSchema).min(1, ""),
});
type FormValues = z.infer<typeof formSchema>;
@@ -78,6 +74,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
fishingLog,
fishingLogIndex,
}) => {
const { t } = useI18n();
const [isCreateMode, setIsCreateMode] = React.useState(!fishingLog?.info);
const [isEditing, setIsEditing] = React.useState(false);
const [expandedFishIndices, setExpandedFishIndices] = React.useState<
@@ -112,7 +109,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
const onSubmit = async (values: FormValues) => {
// Ensure species list is available so we can populate name/rarity
if (!fishSpecies || fishSpecies.length === 0) {
showErrorToast("Danh sách loài cá chưa sẵn sàng");
showErrorToast(t("trip.createHaulModal.fishListNotReady"));
return;
}
// Helper to map form rows -> API info entries (single place)
@@ -134,7 +131,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
try {
const gpsResp = await queryGpsData();
if (!gpsResp.data) {
showErrorToast("Không thể lấy dữ liệu GPS hiện tại");
showErrorToast(t("trip.createHaulModal.gpsError"));
return;
}
const gpsData = gpsResp.data;
@@ -177,21 +174,21 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
if (resp?.status === 200) {
showSuccessToast(
fishingLog?.fishing_log_id == null
? "Thêm mẻ cá thành công"
: "Cập nhật mẻ cá thành công"
? t("trip.createHaulModal.addSuccess")
: t("trip.createHaulModal.updateSuccess")
);
getTrip();
onClose();
} else {
showErrorToast(
fishingLog?.fishing_log_id == null
? "Thêm mẻ cá thất bại"
: "Cập nhật mẻ cá thất bại"
? t("trip.createHaulModal.addError")
: t("trip.createHaulModal.updateError")
);
}
} catch (err) {
console.error("onSubmit error:", err);
showErrorToast("Có lỗi xảy ra khi lưu mẻ cá");
showErrorToast(t("trip.createHaulModal.validationError"));
}
};
@@ -315,7 +312,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
return (
<View style={styles.fishCardHeaderContent}>
<Text style={styles.fishCardTitle}>
{fishName || "Chọn loài cá"}:
{fishName || t("trip.createHaulModal.selectFish")}:
</Text>
<Text style={styles.fishCardSubtitle}>
{fishName ? `${quantity} ${unit}` : "---"}
@@ -335,7 +332,9 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
name={`fish.${index}.id`}
render={({ field: { value, onChange } }) => (
<View style={[styles.fieldGroup, { marginTop: 20 }]}>
<Text style={styles.label}>Tên </Text>
<Text style={styles.label}>
{t("trip.createHaulModal.fishName")}
</Text>
<Select
options={fishSpecies!.map((fish) => ({
label: fish.name,
@@ -343,12 +342,12 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
}))}
value={value}
onChange={onChange}
placeholder="Chọn loài cá"
placeholder={t("trip.createHaulModal.selectFish")}
disabled={!isEditing}
/>
{errors.fish?.[index]?.id && (
<Text style={styles.errorText}>
{errors.fish[index]?.id?.message as string}
{t("trip.createHaulModal.selectFish")}
</Text>
)}
</View>
@@ -363,7 +362,9 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
name={`fish.${index}.quantity`}
render={({ field: { value, onChange, onBlur } }) => (
<View style={styles.fieldGroup}>
<Text style={styles.label}>Số lượng</Text>
<Text style={styles.label}>
{t("trip.createHaulModal.quantity")}
</Text>
<TextInput
keyboardType="numeric"
value={String(value ?? "")}
@@ -379,7 +380,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
/>
{errors.fish?.[index]?.quantity && (
<Text style={styles.errorText}>
{errors.fish[index]?.quantity?.message as string}
{t("trip.createHaulModal.quantity")}
</Text>
)}
</View>
@@ -392,7 +393,9 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
name={`fish.${index}.unit`}
render={({ field: { value, onChange } }) => (
<View style={styles.fieldGroup}>
<Text style={styles.label}>Đơn vị</Text>
<Text style={styles.label}>
{t("trip.createHaulModal.unit")}
</Text>
<Select
options={UNITS_OPTIONS.map((unit) => ({
label: unit.label,
@@ -400,13 +403,13 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
}))}
value={value}
onChange={onChange}
placeholder="Chọn đơn vị"
placeholder={t("trip.createHaulModal.unit")}
disabled={!isEditing}
listStyle={{ maxHeight: 100 }}
/>
{errors.fish?.[index]?.unit && (
<Text style={styles.errorText}>
{errors.fish[index]?.unit?.message as string}
{t("trip.createHaulModal.unit")}
</Text>
)}
</View>
@@ -423,7 +426,10 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
name={`fish.${index}.size`}
render={({ field: { value, onChange, onBlur } }) => (
<View style={styles.fieldGroup}>
<Text style={styles.label}>Kích thước</Text>
<Text style={styles.label}>
{t("trip.createHaulModal.size")} (
{t("trip.createHaulModal.optional")})
</Text>
<TextInput
keyboardType="numeric"
value={value ? String(value) : ""}
@@ -439,7 +445,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
/>
{errors.fish?.[index]?.size && (
<Text style={styles.errorText}>
{errors.fish[index]?.size?.message as string}
{t("trip.createHaulModal.size")}
</Text>
)}
</View>
@@ -452,12 +458,14 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
name={`fish.${index}.sizeUnit`}
render={({ field: { value, onChange } }) => (
<View style={styles.fieldGroup}>
<Text style={styles.label}>Đơn vị</Text>
<Text style={styles.label}>
{t("trip.createHaulModal.unit")}
</Text>
<Select
options={SIZE_UNITS_OPTIONS}
value={value}
onChange={onChange}
placeholder="Chọn đơn vị"
placeholder={t("trip.createHaulModal.unit")}
disabled={!isEditing}
listStyle={{ maxHeight: 80 }}
/>
@@ -488,7 +496,9 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>
{isCreateMode ? "Thêm mẻ cá" : "Chỉnh sửa mẻ cá"}
{isCreateMode
? t("trip.createHaulModal.addFish")
: t("trip.createHaulModal.edit")}
</Text>
<View style={styles.headerButtons}>
{isEditing ? (
@@ -504,14 +514,18 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
{ backgroundColor: "#6c757d" },
]}
>
<Text style={styles.saveButtonText}>Hủy</Text>
<Text style={styles.saveButtonText}>
{t("trip.createHaulModal.cancel")}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={handleSubmit(onSubmit)}
style={styles.saveButton}
>
<Text style={styles.saveButtonText}>Lưu</Text>
<Text style={styles.saveButtonText}>
{t("trip.createHaulModal.save")}
</Text>
</TouchableOpacity>
</>
) : (
@@ -520,7 +534,9 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
onPress={() => setIsEditing(true)}
style={[styles.saveButton, { backgroundColor: "#17a2b8" }]}
>
<Text style={styles.saveButtonText}>Sửa</Text>
<Text style={styles.saveButtonText}>
{t("trip.createHaulModal.edit")}
</Text>
</TouchableOpacity>
)
)}
@@ -546,14 +562,16 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
onPress={() => append(defaultItem())}
style={styles.addButton}
>
<Text style={styles.addButtonText}>+ Thêm loài </Text>
<Text style={styles.addButtonText}>
+ {t("trip.createHaulModal.addFish")}
</Text>
</TouchableOpacity>
)}
{/* Error Message */}
{errors.fish && (
<Text style={styles.errorText}>
{(errors.fish as any)?.message}
{t("trip.createHaulModal.validationError")}
</Text>
)}
</ScrollView>