chore: kanban template

This commit is contained in:
e2e
2026-06-21 18:48:08 +02:00
commit b7cb1beabf
209 changed files with 215578 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import en from './en.json'
// Typed tokens: every `t('key')` is checked against en.json's shape (i18next
// flattens it into dot-path keys), so a typo or removed key is a compile error.
// Other locale files are loaded dynamically and are not key-checked against en;
// a missing key falls back to the default locale at runtime.
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'translation'
resources: {
translation: typeof en
}
}
}
// Add a language: drop `i18n/<lang>.json` in this folder — Vite's
// import.meta.glob auto-registers it below into `resources`, and
// `supportedLocales` is derived from the files present, so no edits are needed
// here. Content is reachable via the `/<lang>` URL prefix (e.g. `/de`, `/es`);
// the default locale (`en`) needs no prefix.
const localeModules = import.meta.glob('./*.json', { eager: true }) as Record<
string,
{ default: Record<string, unknown> }
>
const resources: Record<string, { translation: Record<string, unknown> }> = {}
for (const path in localeModules) {
const code = path.slice(path.lastIndexOf('/') + 1).replace(/\.json$/, '')
resources[code] = { translation: localeModules[path].default }
}
export const supportedLocales = Object.keys(resources)
export type Locale = string
export const defaultLocale = 'en'
const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur'])
// Direction for a locale — RTL for Arabic/Hebrew/Farsi/Urdu, else LTR. Set this
// on <html dir> at the root so the browser mirrors the layout; see the
// pikku-rtl skill. Returns 'ltr' while the app is English-only.
export function localeDir(locale: string = defaultLocale): 'rtl' | 'ltr' {
return RTL_LOCALES.has(locale.split('-')[0]) ? 'rtl' : 'ltr'
}
export function detectLocale(pathname: string): Locale {
const segment = pathname.split('/')[1]
if (supportedLocales.includes(segment as Locale)) {
return segment as Locale
}
if (typeof navigator !== 'undefined') {
const browserLang = navigator.language?.split('-')[0]
if (supportedLocales.includes(browserLang as Locale)) {
return browserLang as Locale
}
}
return defaultLocale
}
// i18n debug mode: when enabled, every *translated* string is masked to block
// glyphs (█), so any readable text left on screen is text that never went
// through a token — a hardcoded/inlined string. Surfaces missing i18n at a
// glance. Toggle with `?i18n-debug` in the URL or `localStorage['i18n-debug']
// = '1'` (or `I18N_DEBUG=1` for server-rendered builds). Off by default.
export function isI18nDebug(): boolean {
if (typeof process !== 'undefined' && process.env?.I18N_DEBUG === '1') return true
if (typeof window === 'undefined') return false
const params = new URLSearchParams(window.location.search)
if (params.has('i18n-debug')) return params.get('i18n-debug') !== '0'
return window.localStorage?.getItem('i18n-debug') === '1'
}
const i18nDebugPostProcessor = {
type: 'postProcessor' as const,
name: 'i18nDebug',
process: (value: string) => (isI18nDebug() ? value.replace(/\S/g, '█') : value),
}
i18n
.use(initReactI18next)
.use(i18nDebugPostProcessor)
.init({
resources,
lng: typeof window !== 'undefined' ? detectLocale(window.location.pathname) : defaultLocale,
fallbackLng: defaultLocale,
interpolation: { escapeValue: false },
postProcess: ['i18nDebug'],
})
export default i18n