webapp-scaffold/README.md
Uwe Schuster ed9863c037 v0.3.0: i18n system (locale × tone) with fallback chain
createI18n({ bundles, defaultLocale, defaultTone, initialLocale,
initialTone, onMiss }) → { t, locale, tone, setLocale, setTone,
subscribe, getSnapshot }

Resolution order:
    <cur-locale>-<cur-tone>
  → <cur-locale>
  → <default-locale>-<default-tone>
  → <default-locale>
  → raw key (onMiss logs first occurrence)

Typed keys via keyof on the caller-supplied Bundle generic. Tone
bundles are Partial<B> so overrides only need the strings that differ
from the locale bundle — the defaultLocale bundle is the only one
expected to be complete.

React bindings isolated in src/i18n-react.ts so the core ships without
a React peer dep. SSR-safe: constructor takes initial locale/tone
rather than reading from window.

Typecheck clean under strict tsc (ES2020/DOM).

Closes fewo-webapp#416

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:12:18 +02:00

102 lines
3.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @uschuster/webapp-scaffold
Shared frontend build glue for webapp-template-derived projects.
## What's in v0.1
| Path | Purpose |
|------|---------|
| `bin/fetch-openapi.sh` | Pull the oatpp Swagger spec from a running backend. Configurable via `OPENAPI_URL` / `APP_URL` / `APP_TEST_PORT`, optional Bearer via `APP_API_KEY`. JSON-validated. |
| `bin/postprocess-openapi.py` | Clean up oatpp 1.3's rough edges (missing `operationId`s, missing tags) before handing the spec to Orval. |
| `bin/inject-hashed-filenames.py` | Rewrite HTML script tags to point at Vite's manifest-declared hashed bundle. Config-driven so projects with multiple entry points (admin + guest) use a single invocation. |
| `src/vite-config.ts` | `defineAdminConfig({root, vendorChunks?, outDir?})` and `defineGuestConfig({root, entry?, vendorChunks?, outDir?})` helpers that bake in the `VITE_BASE`-driven prefix convention, manifest output, and the `static/{dist,guest/dist}` output layout the `StaticController` expects. |
| `templates/orval.config.template.ts` | Starting `orval.config.ts` for derived projects to copy-and-tweak. |
## Install
```json
{
"devDependencies": {
"@uschuster/webapp-scaffold": "^0.1.0"
}
}
```
Register the internal Forgejo npm registry (see your `~/.npmrc`):
```
@uschuster:registry=http://127.0.0.1:3000/api/packages/uwe.admin/npm/
```
## Consumer wiring
```ts
// frontend/vite.config.ts
import { defineAdminConfig } from '@uschuster/webapp-scaffold';
export default defineAdminConfig({ root: __dirname });
```
```sh
# frontend/package.json > scripts > codegen
webapp-scaffold-fetch-openapi && \
webapp-scaffold-postprocess-openapi && \
orval
```
## `createCoreFetch` (v0.2)
Orval's `client: 'fetch'` calls a mutator `coreFetch<T>(url, init)` and
expects `{data, status, headers}` back. `createCoreFetch(opts)` returns
such a function, with credentials/CSRF headers / 204 / content-type
handling already wired, and hooks for project-specific concerns:
```ts
import { createCoreFetch } from '@uschuster/webapp-scaffold/core-fetch';
export const coreFetch = createCoreFetch({
baseUrl: import.meta.env.BASE_URL,
syncTables: new Set(['bookings', 'persons', 'contacts']),
onEnqueue: (req) => syncQueue.push(req), // returns true ⇒ skip network
on401: () => window.location.assign('/admin'),
on409: (_req, body) => ({ conflict: JSON.parse(body) }),
});
```
## i18n (v0.3)
Two axes: **locale** (`de`, `en`, …) × **tone** (`formal`, `informal`).
Bundles are keyed by `<locale>` (fallback) and `<locale>-<tone>` (primary);
the resolver walks `<cur-locale>-<cur-tone>``<cur-locale>`
`<default-locale>-<default-tone>``<default-locale>` → the raw key
(missing keys log via an `onMiss` hook instead of breaking render).
```ts
// Define bundles — only strings that differ between tones need a
// tone-specific entry. Everything else lives in the locale bundle.
import { createI18n } from '@uschuster/webapp-scaffold/i18n';
import { useI18nStore } from '@uschuster/webapp-scaffold/i18n-react'; // React-only
const bundles = {
'de': { greeting: 'Willkommen', see_more: 'Mehr anzeigen' },
'de-informal': { greeting: 'Hi!' },
'en': { greeting: 'Welcome', see_more: 'See more' },
} as const;
export const i18n = createI18n<typeof bundles['de']>({
bundles,
defaultLocale: 'de',
defaultTone: 'formal',
onMiss: (key, loc, tone) =>
console.warn(`[i18n] missing ${String(key)} @ ${loc}-${tone}`),
});
// React — components re-render on setLocale / setTone:
export const useI18n = () => useI18nStore(i18n);
// In a component:
const { t, setTone } = useI18n();
return <button onClick={() => setTone('informal')}>{t('greeting')}</button>;
```
SSR-safe: pass `initialLocale` / `initialTone` from the server render.
Tree-shakeable: only the bundles you import ship in the bundle.