Files
RecipesCam/docker/frontend/src/i18n/I18nProvider.tsx
T
3dtours 8c6e7930db Add self-contained docker/ stack for the web UI
`docker/` now holds the whole web build — frontend (Vite + React + CanvasKit),
backend (Fastify + SQLite) and the compose file — so the folder can be moved to
another machine and run without the React Native project:

    cd docker && cp .env.example .env && docker compose up -d --build

Only `${WEB_PORT:-8090}` is published; nginx serves the SPA and proxies /api to
the `api` container over Docker's DNS. Photos never reach the server.

The shared render code is vendored into `docker/frontend/shared/` and aliased to
a CanvasKit shim, so the app's own frameUtils/toneShader/jpegDpi run unchanged.

Fix the all-black render on GPU surfaces: `MakeWebGLCanvasSurface` creates a
separate WebGL context per call, and a texture from one context cannot be
sampled by a surface on another — so any pass that drew a snapshot onto a second
surface (output sharpen, screen sharpen, polaroid/wallframe cards) came out
solid black, while the raster fallback was correct. Use one shared
GrDirectContext + MakeRenderTarget instead.

Verified in headless Chromium against the running stack: 12MP JPEG in, preview
mean=120.5 sd=60.5, export 2048x1536 mean=107.2 sd=62.1, JFIF density 300/300,
EXIF present, no console errors; health/signup/login/me/recipes all 2xx through
the nginx proxy.
2026-09-17 17:43:03 +07:00

65 lines
1.9 KiB
TypeScript

import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
import { vi, type Dict, type MsgKey } from './vi';
import { en } from './en';
// Two dictionaries, no i18n library: the whole set is a few KB of text and the
// `Dict` type makes a missing English key a build error. Reach for a library
// (or lazy-loading) when the dictionaries outgrow the bundle — not before.
export type Lang = 'vi' | 'en';
const DICTS: Record<Lang, Dict> = { vi, en };
const KEY = 'rc.lang';
function readLang(): Lang {
const stored = localStorage.getItem(KEY);
if (stored === 'vi' || stored === 'en') return stored;
return (navigator.language || '').toLowerCase().startsWith('vi') ? 'vi' : 'en';
}
interface I18nValue {
lang: Lang;
setLang: (lang: Lang) => void;
t: (key: MsgKey, params?: Record<string, string | number>) => string;
}
const Ctx = createContext<I18nValue | null>(null);
export function I18nProvider({ children }: { children: ReactNode }) {
const [lang, setLangState] = useState<Lang>(readLang);
const setLang = useCallback((next: Lang) => {
localStorage.setItem(KEY, next);
setLangState(next);
}, []);
useEffect(() => {
document.documentElement.lang = lang;
}, [lang]);
const value = useMemo<I18nValue>(() => ({
lang,
setLang,
t: (key, params) => {
let text: string = DICTS[lang][key] ?? vi[key] ?? key;
if (params) {
for (const [name, v] of Object.entries(params)) text = text.replace(`{${name}}`, String(v));
}
return text;
},
}), [lang, setLang]);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}
export function useI18n(): I18nValue {
const value = useContext(Ctx);
if (!value) throw new Error('useI18n used outside I18nProvider');
return value;
}
export const LANGS: { id: Lang; label: string }[] = [
{ id: 'vi', label: vi['lang.name'] },
{ id: 'en', label: en['lang.name'] },
];