Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,38 @@ RUN apt-get update && apt-get install openssl -y

# Create a new temp container called `deps` from `base`
# Add the package files and install all the deps.
FROM base AS deps
FROM base AS deps

RUN mkdir /app
WORKDIR /app
RUN mkdir /app
WORKDIR /app

ADD package.json package-lock.json ./
RUN npm install --production=false
ADD package.json package-lock.json ./
RUN npm install --production=false

# create a new temp container called `production-deps` from `base`
# copy the `deps` node_modules folder over and prune it to production only.
FROM base AS production-deps
FROM base AS production-deps

RUN mkdir /app
WORKDIR /app
RUN mkdir /app
WORKDIR /app

COPY --from=deps /app/node_modules /app/node_modules
ADD package.json package-lock.json ./
RUN npm prune --production
COPY --from=deps /app/node_modules /app/node_modules
ADD package.json package-lock.json ./
RUN npm prune --production

# create a new temp container called `build` from `base`
# Copy over the full deps and run build.
FROM base AS build
FROM base AS build

ENV NODE_ENV=production
ENV NODE_ENV=production

RUN mkdir /app
WORKDIR /app
RUN mkdir /app
WORKDIR /app

COPY --from=deps /app/node_modules /app/node_modules
COPY --from=deps /app/node_modules /app/node_modules

ADD . .
RUN npm run build
ADD . .
RUN npm run build

# Go back to the `base` image and copy in the production deps and build
FROM base
Expand All @@ -56,4 +56,4 @@ ADD . .
RUN chmod +x /app/docker-entrypoint.sh

ENTRYPOINT [ "/app/docker-entrypoint.sh" ]
CMD []
CMD []
31 changes: 30 additions & 1 deletion app/lib/broadcast.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,40 @@ export const broadcast = async (zone: string, sounds: string) => {
include: {sounders: {include: {sounder: true}}}
})

let soundQueue: string[]

try {
const parsed = JSON.parse(sounds) as unknown
soundQueue = Array.isArray(parsed) ? parsed : []
} catch {
soundQueue = []
}

if (soundQueue.length === 0) {
return
}

const audio = await prisma.audio.findMany({
where: {id: {in: soundQueue}},
select: {id: true, fileName: true}
})

const audioMap = new Map(audio.map(item => [item.id, item]))

const filteredQueue = soundQueue.filter(id => {
const audioItem = audioMap.get(id)
return Boolean(audioItem?.fileName)
})

if (filteredQueue.length === 0) {
return
}

return asyncForEach(z.sounders, async ({sounder}) => {
await addJob('broadcast', {
ip: sounder.ip,
key: sounder.key,
sounds
sounds: JSON.stringify(filteredQueue)
})
})
}
28 changes: 28 additions & 0 deletions app/lib/i18n.meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {type Messages} from './i18n.shared'

type MatchWithData = {
id: string
data?: unknown
}

const ROOT_ID = 'root'

type RootData = {
locale: string
messages: Messages
}

export const getRootI18n = (matches: MatchWithData[]): RootData => {
const rootMatch = matches.find(match => match.id === ROOT_ID)

if (
rootMatch &&
typeof rootMatch.data === 'object' &&
rootMatch.data !== null
) {
const {locale, messages} = rootMatch.data as RootData
return {locale, messages}
}

return {locale: 'en', messages: {}}
}
62 changes: 62 additions & 0 deletions app/lib/i18n.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {type LoaderFunctionArgs} from '@remix-run/node'

import {

Check warning on line 3 in app/lib/i18n.server.ts

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

Imports "MessageKey" are only used as type
locales,
type SupportedLocale,
FALLBACK_LOCALE,
MessageKey
} from '~/locales'
import {type Messages} from './i18n.shared'

const resolveLocale = (
request: LoaderFunctionArgs['request']
): SupportedLocale => {
const acceptLanguage = request.headers.get('accept-language')

if (acceptLanguage) {
const requestedLocales = acceptLanguage
.split(',')
.map(part => part.split(';')[0]?.trim())
.filter(Boolean) as string[]

for (const requested of requestedLocales) {
const normalized = requested.toLowerCase()

const exactMatch = Object.keys(locales).find(
locale => locale === normalized
)
if (exactMatch) {
return exactMatch as SupportedLocale
}

const prefixMatch = Object.keys(locales).find(locale =>
normalized.startsWith(`${locale.toLowerCase()}-`)
)

if (prefixMatch) {
return prefixMatch as SupportedLocale
}
}
}

return FALLBACK_LOCALE
}

const getMessages = (locale: SupportedLocale): Messages => {
const messages: Messages = {}

;(Object.keys(locales[FALLBACK_LOCALE]) as MessageKey[]).forEach(key => {
messages[key] = locales[locale][key] ?? locales[FALLBACK_LOCALE][key]
})

return messages
}

export const initTranslations = (request: LoaderFunctionArgs['request']) => {
const locale = resolveLocale(request)
const messages = getMessages(locale)

return {locale, messages}
}

export type InitTranslationsReturn = ReturnType<typeof initTranslations>
17 changes: 17 additions & 0 deletions app/lib/i18n.shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type Messages = Record<string, string>

export type TranslateReplacements = Record<string, string | number>

export const translate = (
messages: Messages,
key: string,
replacements: TranslateReplacements = {}
) => {
const template = messages[key] ?? key

return Object.keys(replacements).reduce((acc, replacementKey) => {
const value = replacements[replacementKey]
const pattern = new RegExp(`{{\\s*${replacementKey}\\s*}}`, 'g')
return acc.replace(pattern, String(value))
}, template)
}
42 changes: 42 additions & 0 deletions app/lib/i18n.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {createContext, useContext} from 'react'

import {
translate as baseTranslate,
type Messages,
type TranslateReplacements
} from './i18n.shared'
import {type MessageKey} from '~/locales'

type I18nContextValue = {
locale: string
messages: Messages
}

const I18nContext = createContext<I18nContextValue>({
locale: 'en',
messages: {}
})

export const I18nProvider: React.FC<{
locale: string
messages: Messages
children: React.ReactNode
}> = ({locale, messages, children}) => {
return (
<I18nContext.Provider value={{locale, messages}}>
{children}
</I18nContext.Provider>
)
}

export const useTranslation = () => {
const context = useContext(I18nContext)

const t = (key: MessageKey, replacements: TranslateReplacements = {}) => {
return baseTranslate(context.messages, key, replacements)
}

return {t, locale: context.locale}
}

export const translate = baseTranslate
7 changes: 5 additions & 2 deletions app/lib/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {NavLink} from '@remix-run/react'

import {docsLink} from './utils'
import {useTranslation} from './i18n'

export const SidebarLink: React.FC<{
to: string
Expand Down Expand Up @@ -28,15 +29,17 @@ export const Page: React.FC<{
wide?: boolean
helpLink?: string
}> = ({title, wide, children, helpLink}) => {
const {t} = useTranslation()

return (
<div className={`grid ${wide ? 'grid-cols-wide' : 'grid-cols-narrow'}`}>
<div className="col-start-2">
<h1 className="font-light mb-2 pb-2 border-b-stone-200 border-b">
{title}
{helpLink ? (
<span className="float-right text-base pt-3">
<a href={docsLink(helpLink)} target="_blank">
📖 Docs
<a href={docsLink(helpLink)} target="_blank" rel="noreferrer">
📖 {t('ui.docs')}
</a>
</span>
) : (
Expand Down
Loading
Loading