fix: list data store and list edit form

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-09-26 10:49:38 +08:00
parent 9fe48c5713
commit f420e78ca9
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
37 changed files with 242 additions and 175 deletions

View File

@ -4,14 +4,14 @@ import { useWhoami } from "~/atoms/user"
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip"
import { cn } from "~/lib/utils"
import type { TargetModel } from "~/models"
import type { FeedOrListRespModel } from "~/models"
import { usePresentUserProfileModal } from "~/modules/profile/hooks"
export const FeedCertification = ({
feed,
className,
}: {
feed: TargetModel
feed: FeedOrListRespModel
className?: string
}) => {
const me = useWhoami()

View File

@ -6,7 +6,7 @@ import { forwardRef, useMemo } from "react"
import { getColorScheme, stringToHue } from "~/lib/color"
import { getImageProxyUrl } from "~/lib/img-proxy"
import { cn, getUrlIcon } from "~/lib/utils"
import type { CombinedEntryModel, FeedModel, TargetModel } from "~/models"
import type { CombinedEntryModel, FeedModel, FeedOrListRespModel } from "~/models"
import { PlatformIcon } from "./ui/platform-icon"
@ -76,7 +76,7 @@ export function FeedIcon({
siteUrl,
useMedia,
}: {
feed?: TargetModel
feed?: FeedOrListRespModel
entry?: CombinedEntryModel["entries"]
fallbackUrl?: string
className?: string

View File

@ -2,7 +2,7 @@ import { WEB_URL } from "@follow/shared/constants"
import { FeedIcon } from "~/components/feed-icon"
import { cn } from "~/lib/utils"
import type { TargetModel } from "~/models"
import type { FeedOrListRespModel } from "~/models"
import { FeedCertification } from "./feed-certification"
import { EllipsisHorizontalTextWithTooltip } from "./ui/typography"
@ -12,7 +12,7 @@ export function FollowSummary({
docs,
className,
}: {
feed: TargetModel
feed: FeedOrListRespModel
docs?: string
className?: string
}) {

View File

@ -80,7 +80,7 @@ export const RelativeTime: FC<{
{/* https://github.com/radix-ui/primitives/issues/2248#issuecomment-2147056904 */}
<TooltipTrigger onFocusCapture={stopPropagation}>
{relative}
{t("words.space")}
{t("space")}
{t("words.ago")}
</TooltipTrigger>

View File

@ -1,8 +1,8 @@
import type { TargetModel } from "~/models"
import type { FeedOrListRespModel } from "~/models"
export type DB_FeedUnread = {
id: string
count: number
}
export type DB_Feed = TargetModel & { id: string }
export type DB_Feed = FeedOrListRespModel & { id: string }

View File

@ -25,9 +25,10 @@ export type TransactionModel = ExtractBizResponse<
export type FeedModel = ExtractBizResponse<typeof apiClient.feeds.$get>["data"]["feed"]
export type ListModel = ExtractBizResponse<typeof apiClient.lists.$get>["data"]["list"]
export type TargetModel = FeedModel | ListModel
type ListModelPoplutedFeeds = ExtractBizResponse<typeof apiClient.lists.$get>["data"]["list"]
export type ListModel = Omit<ListModelPoplutedFeeds, "feeds">
export type FeedOrListRespModel = FeedModel | ListModelPoplutedFeeds
export type FeedOrListModel = FeedModel | ListModel
export type EntryResponse = Exclude<
Extract<ExtractBizResponse<typeof apiClient.entries.$get>, { code: 0 }>["data"],

View File

@ -99,7 +99,7 @@ export const EntryItemWrapper: FC<
},
{
type: "text" as const,
label: `${t("words.copy")}${t("words.space")}${t("words.entry")} ${t("words.id")}`,
label: `${t("words.copy")}${t("space")}${t("words.entry")} ${t("words.id")}`,
click: () => {
navigator.clipboard.writeText(entry.entries.id)
},

View File

@ -53,7 +53,7 @@ export const EntryListHeader: FC<{
<div className="text-xs font-medium text-zinc-400">
{totalCount || 0} {t("quantifier.piece", { ns: "common" })}
{unreadOnly && !isInCollectionList ? t("words.unread") : ""}
{t("words.space", { ns: "common" })}
{t("space", { ns: "common" })}
{t("words.items", { ns: "common", count: totalCount })}
</div>
</div>

View File

@ -1,11 +1,11 @@
import type { FC } from "react"
import type { CombinedEntryModel, TargetModel } from "~/models"
import type { CombinedEntryModel, FeedOrListRespModel } from "~/models"
export type UniversalItemProps = {
entryId: string
entryPreview?: CombinedEntryModel & {
feeds: TargetModel
feeds: FeedOrListRespModel
feedId: string
}
translation?: {

View File

@ -318,7 +318,7 @@ const SearchResultCount: FC<{
<>
{count} {t.common("quantifier.piece")}
{t.common("words.local")}
{t.common("words.space")}
{t.common("space")}
{t.common("words.record", { count })}
</>
)}

View File

@ -40,17 +40,20 @@ import { views } from "~/constants"
import { useAuthQuery, useI18n } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { cn, isBizId } from "~/lib/utils"
import type { ListModel } from "~/models"
import type { FeedModel, ListModel } from "~/models"
import { ViewSelectorRadioGroup } from "~/modules/shared/ViewSelectorRadioGroup"
import { Balance } from "~/modules/wallet/balance"
import { Queries } from "~/queries"
import { feedActions, getFeedById, useFeedById } from "~/store/feed"
import {
getFeedById,
useAddFeedToFeedList,
useFeedById,
useRemoveFeedFromFeedList,
} from "~/store/feed"
import { useSubscriptionStore } from "~/store/subscription"
export const SettingLists = () => {
// const { t: appT } = useTranslation()
const t = useI18n()
// const { t } = useTranslation("settings")
const listList = useAuthQuery(Queries.lists.list())
const { present } = useModalStack()
@ -342,42 +345,7 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
const { t } = useTranslation("settings")
const [feedSearchFor, setFeedSearchFor] = useState("")
const addMutation = useMutation({
mutationFn: async (values: string) => {
const feed = await apiClient.lists.feeds.$post({
json: {
listId: id,
feedId: values,
},
})
feedActions.upsertMany([feed.data])
},
onSuccess: () => {
toast.success(t("lists.feeds.add.success"))
Queries.lists.list().invalidate()
},
async onError() {
toast.error(t("lists.feeds.add.error"))
},
})
const removeMutation = useMutation({
mutationFn: async (feedId: string) => {
await apiClient.lists.feeds.$delete({
json: {
listId: id,
feedId,
},
})
},
onSuccess: () => {
toast.success(t("lists.feeds.delete.success"))
Queries.lists.list().invalidate()
},
async onError() {
toast.error(t("lists.feeds.delete.error"))
},
})
const addMutation = useAddFeedToFeedList()
const allFeeds = useSubscriptionStore((store) => {
const feedInfo = [] as { title: string; id: string }[]
@ -395,11 +363,13 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
})
const autocompleteSuggestions: Suggestion[] = useMemo(() => {
return allFeeds.map((feed) => ({
name: feed.title,
value: feed.id,
}))
}, [allFeeds])
return allFeeds
.filter((feed) => !list.feedIds?.includes(feed.id))
.map((feed) => ({
name: feed.title,
value: feed.id,
}))
}, [allFeeds, list.feedIds])
const selectedFeedIdRef = useRef<string | null>()
return (
@ -422,11 +392,11 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
className="whitespace-nowrap"
onClick={() => {
if (isBizId(feedSearchFor)) {
addMutation.mutate(feedSearchFor)
addMutation.mutate({ feedId: feedSearchFor, listId: id })
return
}
if (selectedFeedIdRef.current) {
addMutation.mutate(selectedFeedIdRef.current)
addMutation.mutate({ feedId: selectedFeedIdRef.current, listId: id })
}
}}
>
@ -438,9 +408,6 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
<Table className="mt-4">
<TableHeader className="border-b">
<TableRow className="[&_*]:!font-semibold">
{/* <TableHead className="w-20 text-center" size="sm">
{t("lists.feeds.id")}
</TableHead> */}
<TableHead size="sm" className="pl-8">
{t("lists.feeds.title")}
</TableHead>
@ -453,36 +420,41 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
</TableRow>
</TableHeader>
<TableBody className="border-t-[12px] border-transparent">
{list.feeds?.map((row) => (
<TableRow key={row.title} className="h-8">
{/* <TableCell align="center" size="sm">
{row.id}
</TableCell> */}
<TableCell size="sm">
<a
target="_blank"
href={`${WEB_URL}/list/${row.id}`}
className="flex items-center gap-2 font-semibold"
>
{row.siteUrl && <FeedIcon className="mr-0" siteUrl={row.siteUrl} />}
<span className="inline-block max-w-[200px] truncate">{row.title}</span>
</a>
</TableCell>
<TableCell align="center" size="sm">
<div className="center">
<FeedCertification className="ml-0" feed={row} />
</div>
</TableCell>
<TableCell align="center" size="sm">
<Button variant="ghost" onClick={() => removeMutation.mutate(row.id)}>
<i className="i-mgc-delete-2-cute-re" />
</Button>
</TableCell>
</TableRow>
))}
{list.feedIds?.map((feedId) => <RowRender feedId={feedId} key={feedId} listId={id} />)}
</TableBody>
</Table>
</ScrollArea.ScrollArea>
</>
)
}
const RowRender = ({ feedId, listId }: { feedId: string; listId: string }) => {
const feed = useFeedById(feedId) as FeedModel
const removeMutation = useRemoveFeedFromFeedList()
if (!feed) return null
return (
<TableRow key={feed.title} className="h-8">
<TableCell size="sm">
<a
target="_blank"
href={`${WEB_URL}/list/${feed.id}`}
className="flex items-center gap-2 font-semibold"
>
{feed.siteUrl && <FeedIcon className="mr-0" siteUrl={feed.siteUrl} />}
<span className="inline-block max-w-[200px] truncate">{feed.title}</span>
</a>
</TableCell>
<TableCell align="center" size="sm">
<div className="center">
<FeedCertification className="ml-0" feed={feed} />
</div>
</TableCell>
<TableCell align="center" size="sm">
<Button variant="ghost" onClick={() => removeMutation.mutate({ feedId: feed.id, listId })}>
<i className="i-mgc-delete-2-cute-re" />
</Button>
</TableCell>
</TableRow>
)
}

View File

@ -12,6 +12,7 @@ import { usePresentFeedFormModal } from "~/hooks/biz/useFeedFormModal"
import { useTitle } from "~/hooks/common"
import type { ListModel } from "~/models"
import { useFeed } from "~/queries/feed"
import { useFeedById } from "~/store/feed/hooks"
export function Component() {
const { id } = useParams()
@ -51,7 +52,7 @@ export function Component() {
subscriptionCount: feed.data.subscriptionCount,
subscriptionNoun: t("feed.follower", { count: feed.data.subscriptionCount }),
feedsCount: "feedCount" in feed.data ? feed.data.feedCount : 0,
feedsNoun: t("feed.feeds", { count: listData?.feeds?.length }),
feedsNoun: t("feed.feeds", { count: listData?.feedIds?.length }),
appName: APP_NAME,
})}
</div>
@ -78,23 +79,9 @@ export function Component() {
</Button>
</span>
<div className="flex w-full max-w-3xl flex-col gap-4 pb-12 pt-8">
{listData.feeds?.slice(0, 5).map((feed) => (
<a
className="relative flex cursor-pointer items-center text-base"
href={`/feed/${feed.id}`}
target="_blank"
key={feed.id}
>
<FeedIcon
fallback
feed={feed}
className="mask-squircle mask mr-2 shrink-0"
size={20}
/>
{feed.title}
<FeedCertification feed={feed} />
</a>
))}
{listData.feedIds
?.slice(0, 5)
.map((feedId) => <FeedRow feedId={feedId} key={feedId} />)}
{"feedCount" in feed.data && (
<div
onClick={() => {
@ -117,3 +104,20 @@ export function Component() {
</>
)
}
const FeedRow = ({ feedId }: { feedId: string }) => {
const feed = useFeedById(feedId)
if (!feed) return null
return (
<a
className="relative flex cursor-pointer items-center text-base"
href={`/feed/${feed.id}`}
target="_blank"
key={feed.id}
>
<FeedIcon fallback feed={feed} className="mask-squircle mask mr-2 shrink-0" size={20} />
{feed.title}
<FeedCertification feed={feed} />
</a>
)
}

View File

@ -1,23 +1,25 @@
import { browserDB } from "~/database"
import type { TargetModel } from "~/models/types"
import type { FeedOrListModel } from "~/models/types"
import { BaseService } from "./base"
import { CleanerService } from "./cleaner"
type TargetModelWithId = TargetModel & { id: string }
type TargetModelWithId = FeedOrListModel & { id: string }
class ServiceStatic extends BaseService<TargetModelWithId> {
constructor() {
super(browserDB.feeds)
}
override async upsertMany(data: TargetModel[]) {
override async upsertMany(data: FeedOrListModel[]) {
const filterData = data.filter((d) => d.id)
CleanerService.reset(filterData.map((d) => ({ type: "feed", id: d.id! })))
return this.table.bulkPut(filterData as TargetModelWithId[])
}
override async upsert(data: TargetModel): Promise<string | null> {
override async upsert(data: FeedOrListModel): Promise<string | null> {
if (!data.id) return null
CleanerService.reset([{ type: "feed", id: data.id }])
return this.table.put(data as TargetModelWithId)

View File

@ -5,7 +5,7 @@ import { isNil, merge, omit } from "lodash-es"
import { runTransactionInScope } from "~/database"
import { apiClient } from "~/lib/api-fetch"
import { getEntriesParams, omitObjectUndefinedValue } from "~/lib/utils"
import type { CombinedEntryModel, EntryModel, TargetModel } from "~/models"
import type { CombinedEntryModel, EntryModel, FeedOrListRespModel } from "~/models"
import { EntryService } from "~/services"
import { feedActions } from "../feed"
@ -178,7 +178,7 @@ class EntryActions {
}
upsertMany(data: CombinedEntryModel[]) {
const feeds = [] as TargetModel[]
const feeds = [] as FeedOrListRespModel[]
const entries = [] as EntryModel[]
const entry2Read = {} as Record<string, boolean>
const entryFeedMap = {} as Record<string, string>

View File

@ -1,16 +1,19 @@
import { useMutation } from "@tanstack/react-query"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { useShallow } from "zustand/react/shallow"
import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER, ROUTE_FEED_PENDING, views } from "~/constants"
import { useRouteParams } from "~/hooks/biz/useRouteParams"
import type { TargetModel } from "~/models"
import { apiClient } from "~/lib/api-fetch"
import type { FeedOrListRespModel } from "~/models"
import { getSubscriptionByFeedId } from "../subscription"
import { useFeedStore } from "./store"
import { feedActions, useFeedStore } from "./store"
import type { FeedQueryParams } from "./types"
export const useFeedById = (feedId: Nullable<string>): TargetModel | null =>
export const useFeedById = (feedId: Nullable<string>): FeedOrListRespModel | null =>
useFeedStore((state) => (feedId ? state.feeds[feedId] : null))
export const useFeedByIdOrUrl = (feed: FeedQueryParams) =>
@ -26,7 +29,7 @@ export const useFeedByIdOrUrl = (feed: FeedQueryParams) =>
export const useFeedByIdSelector = <T>(
feedId: Nullable<string>,
selector: (feed: TargetModel) => T,
selector: (feed: FeedOrListRespModel) => T,
) =>
useFeedStore(
useShallow((state) => (feedId && state.feeds[feedId] ? selector(state.feeds[feedId]) : null)),
@ -45,7 +48,6 @@ export const useFeedHeaderTitle = () => {
switch (currentFeedId) {
case ROUTE_FEED_PENDING: {
// TODO: fix this type error
return t(views[view].name)
}
case FEED_COLLECTION_LIST: {
@ -59,3 +61,54 @@ export const useFeedHeaderTitle = () => {
}
}
}
export const useAddFeedToFeedList = (options?: { onSuccess: () => void; onError: () => void }) => {
const { t } = useTranslation("settings")
return useMutation({
mutationFn: async (payload: { feedId: string; listId: string }) => {
const feed = await apiClient.lists.feeds.$post({
json: {
listId: payload.listId,
feedId: payload.feedId,
},
})
feedActions.addFeedToFeedList(payload.listId, feed.data)
},
onSuccess: () => {
toast.success(t("lists.feeds.add.success"))
options?.onSuccess?.()
},
async onError() {
toast.error(t("lists.feeds.add.error"))
options?.onError?.()
},
})
}
export const useRemoveFeedFromFeedList = (options?: {
onSuccess: () => void
onError: () => void
}) => {
const { t } = useTranslation("settings")
return useMutation({
mutationFn: async (payload: { feedId: string; listId: string }) => {
feedActions.removeFeedFromFeedList(payload.listId, payload.feedId)
await apiClient.lists.feeds.$delete({
json: {
listId: payload.listId,
feedId: payload.feedId,
},
})
},
onSuccess: () => {
toast.success(t("lists.feeds.delete.success"))
options?.onSuccess?.()
},
async onError() {
toast.error(t("lists.feeds.delete.error"))
options?.onError?.()
},
})
}

View File

@ -1,10 +1,17 @@
import { produce } from "immer"
import { omit } from "lodash-es"
import { nanoid } from "nanoid"
import { whoami } from "~/atoms/user"
import { runTransactionInScope } from "~/database"
import { apiClient } from "~/lib/api-fetch"
import type { TargetModel, UserModel } from "~/models"
import type {
FeedModel,
FeedOrListModel,
FeedOrListRespModel,
ListModel,
UserModel,
} from "~/models"
import { FeedService } from "~/services"
import { getSubscriptionByFeedId } from "../subscription"
@ -23,9 +30,16 @@ class FeedActions {
set({ feeds: {} })
}
upsertMany(feeds: TargetModel[]) {
upsertMany(feeds: FeedOrListRespModel[]) {
runTransactionInScope(() => {
FeedService.upsertMany(feeds)
FeedService.upsertMany(
feeds.map((feed) => {
if (feed.type === "list") {
return omit(feed, ["feeds"])
}
return feed
}),
)
})
set((state) =>
produce(state, (state) => {
@ -53,7 +67,7 @@ class FeedActions {
}
})
state.feeds[feed.id] = feed
state.feeds[feed.id] = omit(feed, "feeds") as FeedOrListModel
} else {
// Store temp feed in memory
const nonce = feed["nonce"] || nanoid(8)
@ -67,7 +81,7 @@ class FeedActions {
)
}
private patch(feedId: string, patch: Partial<TargetModel>) {
private patch(feedId: string, patch: Partial<FeedOrListRespModel>) {
set((state) =>
produce(state, (state) => {
const feed = state.feeds[feedId]
@ -100,6 +114,27 @@ class FeedActions {
})
}
async addFeedToFeedList(listId: string, feed: FeedModel) {
const list = get().feeds[listId] as ListModel
if (!list) return
feedActions.upsertMany([feed])
this.patch(listId, {
feedIds: [feed.id, ...list.feedIds],
})
feedActions.upsertMany([get().feeds[listId]])
}
async removeFeedFromFeedList(listId: string, feedId: string) {
const list = get().feeds[listId] as ListModel
if (!list) return
this.patch(listId, {
feedIds: list.feedIds.filter((id) => id !== feedId),
})
feedActions.upsertMany([get().feeds[listId]])
}
// API Fetcher
//
@ -136,17 +171,17 @@ class FeedActions {
async fetchOwnedLists() {
const res = await apiClient.lists.list.$get()
this.upsertMany(res.data)
this.upsertMany(res.data.concat())
return res.data
}
}
export const feedActions = new FeedActions()
export const getFeedById = (feedId: string): Nullable<TargetModel> =>
export const getFeedById = (feedId: string): Nullable<FeedOrListRespModel> =>
useFeedStore.getState().feeds[feedId]
export const getPreferredTitle = (feed?: TargetModel | null) => {
export const getPreferredTitle = (feed?: FeedOrListRespModel | null) => {
if (!feed?.id) {
return feed?.title
}

View File

@ -1,15 +1,15 @@
import type { TargetModel } from "~/models"
import type { FeedOrListModel, FeedOrListRespModel } from "~/models"
type FeedId = string
export interface FeedState {
feeds: Record<FeedId, TargetModel>
feeds: Record<FeedId, FeedOrListModel>
}
export interface FeedActions {
upsertMany: (feeds: TargetModel[]) => void
upsertMany: (feeds: FeedOrListRespModel[]) => void
clear: () => void
patch: (feedId: FeedId, patch: Partial<TargetModel>) => void
patch: (feedId: FeedId, patch: Partial<FeedOrListModel>) => void
}
export type FeedQueryParams = { id?: string; url?: string; isList?: boolean }

View File

@ -1,4 +1,4 @@
import type { EntryModel, TargetModel } from "~/models"
import type { EntryModel, FeedOrListRespModel } from "~/models"
import type { SubscriptionFlatModel } from "../subscription"
import type { SearchType } from "./constants"
@ -9,7 +9,7 @@ export interface SearchResult<T extends object, A extends object = object> exten
}
export interface SearchState {
feeds: SearchResult<TargetModel>[]
feeds: SearchResult<FeedOrListRespModel>[]
entries: SearchResult<EntryModel, { feedId: string }>[]
subscriptions: SearchResult<SubscriptionFlatModel, { feedId: string }>[]

View File

@ -5,6 +5,7 @@
"confirm": "تأكيد",
"ok": "موافق",
"quantifier.piece": "",
"space": " ",
"time.last_night": "الليلة الماضية",
"time.the_night_before_last": "الليلة التي قبل الماضية",
"time.today": "اليوم",
@ -24,6 +25,5 @@
"words.result": "نتيجة",
"words.result_one": "نتيجة",
"words.result_other": "نتائج",
"words.space": " ",
"words.which.all": "الكل"
}
}

View File

@ -4,6 +4,7 @@
"confirm": "تأكيد",
"ok": "موافق",
"quantifier.piece": "",
"space": " ",
"time.last_night": "الليلة الماضية",
"time.the_night_before_last": "الليلة قبل الماضية",
"time.today": "اليوم",
@ -23,6 +24,5 @@
"words.result": "نتيجة",
"words.result_one": "نتيجة",
"words.result_other": "نتائج",
"words.space": " ",
"words.which.all": "الكل"
}
}

View File

@ -4,6 +4,7 @@
"confirm": "تأكيد",
"ok": "موافق",
"quantifier.piece": "",
"space": " ",
"time.last_night": "الليلة الماضية",
"time.the_night_before_last": "الليلة قبل الماضية",
"time.today": "اليوم",
@ -23,6 +24,5 @@
"words.result": "نتيجة",
"words.result_one": "نتيجة",
"words.result_other": "نتائج",
"words.space": " ",
"words.which.all": "الكل"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "تأكيد",
"ok": "موافق",
"quantifier.piece": "",
"space": " ",
"time.last_night": "الليلة الماضية",
"time.the_night_before_last": "الليلة التي سبقتها",
"time.today": "اليوم",
@ -24,6 +25,5 @@
"words.result": "نتيجة",
"words.result_one": "نتيجة",
"words.result_other": "نتائج",
"words.space": " ",
"words.which.all": "الكل"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "تأكيد",
"ok": "حسنًا",
"quantifier.piece": "",
"space": " ",
"time.last_night": "الليلة الماضية",
"time.the_night_before_last": "الليلة قبل الماضية",
"time.today": "اليوم",
@ -24,6 +25,5 @@
"words.result": "نتيجة",
"words.result_one": "نتيجة",
"words.result_other": "نتائج",
"words.space": " ",
"words.which.all": "الكل"
}
}

View File

@ -4,6 +4,7 @@
"confirm": "تأكيد",
"ok": "موافق",
"quantifier.piece": "",
"space": " ",
"time.last_night": "الليلة الماضية",
"time.the_night_before_last": "الليلة التي قبلها",
"time.today": "اليوم",
@ -23,6 +24,5 @@
"words.result": "نتيجة",
"words.result_one": "نتيجة",
"words.result_other": "نتائج",
"words.space": " ",
"words.which.all": "الكل"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "Confirm",
"ok": "OK",
"quantifier.piece": "",
"space": " ",
"time.last_night": "Last Night",
"time.the_night_before_last": "The Night Before Last",
"time.today": "Today",
@ -28,8 +29,7 @@
"words.result": "result",
"words.result_one": "result",
"words.result_other": "results",
"words.space": " ",
"words.submit": "Submit",
"words.update": "Update",
"words.which.all": "All"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "Confirmar",
"ok": "OK",
"quantifier.piece": "",
"space": " ",
"time.last_night": "Anoche",
"time.the_night_before_last": "Anteanoche",
"time.today": "Hoy",
@ -24,6 +25,5 @@
"words.result": "resultado",
"words.result_one": "resultado",
"words.result_other": "resultados",
"words.space": " ",
"words.which.all": "Todo"
}
}

View File

@ -4,6 +4,7 @@
"confirm": "Vahvista",
"ok": "OK",
"quantifier.piece": "",
"space": " ",
"time.last_night": "Viime yö",
"time.the_night_before_last": "Toissa yö",
"time.today": "Tänään",
@ -23,6 +24,5 @@
"words.result": "tulos",
"words.result_one": "tulos",
"words.result_other": "tulokset",
"words.space": " ",
"words.which.all": "Kaikki"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "Confirmer",
"ok": "OK",
"quantifier.piece": "",
"space": " ",
"time.last_night": "La nuit dernière",
"time.the_night_before_last": "L'avant-dernière nuit",
"time.today": "Aujourd'hui",
@ -24,6 +25,5 @@
"words.result": "résultat",
"words.result_one": "résultat",
"words.result_other": "résultats",
"words.space": " ",
"words.which.all": "Tous"
}
}

View File

@ -4,6 +4,7 @@
"confirm": "Conferma",
"ok": "OK",
"quantifier.piece": "",
"space": " ",
"time.last_night": "Ieri sera",
"time.the_night_before_last": "L'altra sera",
"time.today": "Oggi",
@ -23,6 +24,5 @@
"words.result": "risultato",
"words.result_one": "risultato",
"words.result_other": "risultati",
"words.space": " ",
"words.which.all": "Tutti"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "確認",
"ok": "OK",
"quantifier.piece": "個",
"space": "",
"time.last_night": "昨夜",
"time.the_night_before_last": "一昨夜",
"time.today": "今日",
@ -24,6 +25,5 @@
"words.result": "結果",
"words.result_one": "結果",
"words.result_other": "結果",
"words.space": "",
"words.which.all": "すべて"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "확인",
"ok": "확인",
"quantifier.piece": "",
"space": " ",
"time.last_night": "어젯밤",
"time.the_night_before_last": "그저께 밤",
"time.today": "오늘",
@ -27,8 +28,7 @@
"words.result": "결과",
"words.result_one": "결과",
"words.result_other": "결과들",
"words.space": " ",
"words.submit": "제출",
"words.update": "업데이트",
"words.which.all": "모두"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "Confirmar",
"ok": "OK",
"quantifier.piece": "",
"space": " ",
"time.last_night": "Ontem à noite",
"time.the_night_before_last": "Anteontem à noite",
"time.today": "Hoje",
@ -24,6 +25,5 @@
"words.result": "resultado",
"words.result_one": "resultado",
"words.result_other": "resultados",
"words.space": " ",
"words.which.all": "Todos"
}
}

View File

@ -4,6 +4,7 @@
"confirm": "Подтвердить",
"ok": "ОК",
"quantifier.piece": "",
"space": " ",
"time.last_night": "Прошлой ночью",
"time.the_night_before_last": "Позапрошлой ночью",
"time.today": "Сегодня",
@ -23,6 +24,5 @@
"words.result": "результат",
"words.result_one": "результат",
"words.result_other": "результаты",
"words.space": " ",
"words.which.all": "Все"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "Onayla",
"ok": "Tamam",
"quantifier.piece": "",
"space": " ",
"time.last_night": "Dün Gece",
"time.the_night_before_last": "Evvelsi Gece",
"time.today": "Bugün",
@ -27,8 +28,7 @@
"words.result": "sonuç",
"words.result_one": "sonuç",
"words.result_other": "sonuçlar",
"words.space": " ",
"words.submit": "Gönder",
"words.update": "Güncelle",
"words.which.all": "Tümü"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "确认",
"ok": "好",
"quantifier.piece": "条",
"space": "",
"time.last_night": "昨晚",
"time.the_night_before_last": "前天晚上",
"time.today": "今天",
@ -28,8 +29,7 @@
"words.result": "结果",
"words.result_one": "结果",
"words.result_other": "结果",
"words.space": "",
"words.submit": "提交",
"words.update": "更新",
"words.which.all": "全部"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "確認",
"ok": "確定",
"quantifier.piece": "",
"space": " ",
"time.last_night": "昨晚",
"time.the_night_before_last": "前晚",
"time.today": "今天",
@ -27,8 +28,7 @@
"words.result": "結果",
"words.result_one": "結果",
"words.result_other": "結果",
"words.space": " ",
"words.submit": "提交",
"words.update": "更新",
"words.which.all": "全部"
}
}

View File

@ -5,6 +5,7 @@
"confirm": "確認",
"ok": "確定",
"quantifier.piece": "",
"space": " ",
"time.last_night": "昨晚",
"time.the_night_before_last": "前晚",
"time.today": "今天",
@ -24,6 +25,5 @@
"words.result": "結果",
"words.result_one": "結果",
"words.result_other": "多筆結果",
"words.space": " ",
"words.which.all": "全部"
}
}