Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ node_modules
/formatters
/importer
/parser
/preact
/react
/runtime
/schema
Expand All @@ -20,4 +21,4 @@ packages/generator/test/generated/console*
/typesafe-i18n-*.tgz
/types
/.eslintcache
.pnpm-debug.log
.pnpm-debug.log
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Click [here](https://codesandbox.io/s/typesafe-i18n-demo-qntgqy?file=/index.ts)
<a title="React" href="https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-react">
<img src="https://raw.githubusercontent.com/ivanhofer/typesafe-i18n/main/assets/icons/react.svg" height="70" hspace="10">
</a>
<a title="Preact" href="https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-preact">
<img src="https://raw.githubusercontent.com/ivanhofer/typesafe-i18n/main/assets/icons/preact.svg" height="70" hspace="10">
</a>
<a title="Vue.js" href="https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-vue">
<img src="https://raw.githubusercontent.com/ivanhofer/typesafe-i18n/main/assets/icons/vuejs.svg" height="70" hspace="10">
</a>
Expand Down Expand Up @@ -162,6 +165,7 @@ You can use `typesafe-i18n` in a variety of project-setups:
- [Angular](https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-angular) applications
- [Node.js](https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-node) apis, backends, scripts, ...
- [React / Next.js](https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-react) applications
- [Preact](https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-preact) applications
- [Solid.js](https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-solid) applications
- [Svelte / SvelteKit / Sapper](https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-svelte) applications
- [Vue.js / Nuxt.js](https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/adapter-vue) applications
Expand Down
9 changes: 9 additions & 0 deletions assets/icons/preact.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"svelte",
"sveltekit",
"react",
"preact",
"react-native",
"nextjs",
"expo",
Expand Down Expand Up @@ -71,6 +72,11 @@
"import": "./parser/index.mjs",
"require": "./parser/index.cjs"
},
"./preact": {
"types": "./preact/index.d.ts",
"import": "./preact/index.mjs",
"require": "./preact/index.cjs"
},
"./react": {
"types": "./react/index.d.ts",
"import": "./react/index.mjs",
Expand Down Expand Up @@ -113,6 +119,7 @@
"/formatters",
"/importer",
"/parser",
"/preact",
"/react",
"/runtime",
"/schema",
Expand All @@ -126,7 +133,7 @@
"-- DEV -------------------------------------------------": "",
"update:dependencies": "pnpm up -r",
"update:version": "tsx ./update-version.ts",
"clear": "rimraf ./angular ./cli ./config ./detectors ./dist ./exporter ./formatters ./importer ./parser ./react ./runtime ./schema ./solid ./svelte ./types ./utils ./vue ./temp-output",
"clear": "rimraf ./angular ./cli ./config ./detectors ./dist ./exporter ./formatters ./importer ./parser ./preact ./react ./runtime ./schema ./solid ./svelte ./types ./utils ./vue ./temp-output",
"-- LINT ------------------------------------------------": "",
"lint": "eslint --cache packages",
"lint:ci": "eslint --cache --fix",
Expand Down
34 changes: 34 additions & 0 deletions packages/adapter-preact/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# `typesafe-i18n` Preact

This package provides a lightweight Preact context wrapper around `typesafe-i18n`.

## Setup

See [here](https://github.com/ivanhofer/typesafe-i18n#get-started) for details on the generator.

Run the setup once to scaffold the boilerplate:

```bash
npx typesafe-i18n --setup-auto
```

The generator will create `i18n-preact.tsx` (name configurable via `adapterFileName`). Wrap your app with the generated component and consume the context with the generated hook:

```tsx
import { render } from 'preact'
import TypesafeI18n, { useI18nContext } from './i18n/i18n-preact'

const App = () => {
const { LL } = useI18nContext()
return <h1>{LL.HI({ name: 'Mauricio' })}</h1>
}

render(
<TypesafeI18n locale="en">
<App />
</TypesafeI18n>,
document.getElementById('app')!,
)
```

See the main README for more advanced usage and loading locales.
47 changes: 47 additions & 0 deletions packages/adapter-preact/esbuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable no-console */
import { context } from 'esbuild'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'

const watch = process.argv.includes('--watch')

//@ts-ignore
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const getPath = (file: string) => resolve(__dirname, file)

const formats = ['esm', 'cjs'] as const

const contexts = await Promise.all(
formats.flatMap((format) =>
[false, true].map((minify) =>
context({
entryPoints: ['./src/index.ts'],
bundle: true,
outfile: getPath(`../../preact/index${minify ? '.min' : ''}.${format === 'esm' ? 'm' : 'c'}js`),
external: ['preact', 'preact/hooks'],
platform: 'browser',
format,
sourcemap: watch,
minify,
tsconfig: './tsconfig.json',
}),
),
),
)

for (const ctx of contexts) {
if (watch) {
await ctx.watch()
console.info('👀 watching for changes...')
process.on('exit', async () => {
console.info('🙈 process killed')
await ctx.dispose()
})
} else {
await ctx.rebuild()
console.info('✅ build complete')
await ctx.dispose()
}
}
15 changes: 15 additions & 0 deletions packages/adapter-preact/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@typesafe-i18n/adapter-preact",
"scripts": {
"dev": "tsx esbuild.ts --watch",
"build": "tsx esbuild.ts && tsc -p tsconfig.json --emitDeclarationOnly",
"test": "tsc --noEmit"
},
"devDependencies": {
"esbuild": "^0.18.18",
"preact": "^10.17.1",
"tsx": "^3.12.7",
"typescript": "^5.1.6"
},
"type": "module"
}
74 changes: 74 additions & 0 deletions packages/adapter-preact/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createContext, h, type ComponentChildren, type Context, type FunctionComponent } from 'preact'
import { useCallback, useMemo, useState } from 'preact/hooks'
import { getFallbackProxy } from '../../runtime/src/core-utils.mjs'
import type { BaseFormatters, BaseTranslation, Locale, TranslationFunctions } from '../../runtime/src/core.mjs'
import { i18nObject } from '../../runtime/src/util.object.mjs'

// --------------------------------------------------------------------------------------------------------------------
// types --------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------------

export type I18nContextType<
L extends Locale = Locale,
T extends BaseTranslation | BaseTranslation[] = BaseTranslation,
TF extends TranslationFunctions<T> = TranslationFunctions<T>,
> = {
locale: L
LL: TF
setLocale: (locale: L) => void
}

export type TypesafeI18nProps<L extends string> = {
locale: L
children: ComponentChildren
}

export type PreactInit<
L extends Locale = Locale,
T extends BaseTranslation | BaseTranslation[] = BaseTranslation,
TF extends TranslationFunctions<T> = TranslationFunctions<T>,
> = {
component: FunctionComponent<TypesafeI18nProps<L>>
context: Context<I18nContextType<L, T, TF>>
}

// --------------------------------------------------------------------------------------------------------------------
// implementation -----------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------------

export const initI18nPreact = <
L extends Locale = Locale,
T extends BaseTranslation = BaseTranslation,
TF extends TranslationFunctions<T> = TranslationFunctions<T>,
F extends BaseFormatters = BaseFormatters,
>(
translations: Record<L, T>,
formatters: Record<L, F> = {} as Record<L, F>,
): PreactInit<L, T, TF> => {
const context = createContext({} as I18nContextType<L, T, TF>)

const component: FunctionComponent<TypesafeI18nProps<L>> = (props) => {
const [locale, _setLocale] = useState<L>(props.locale)
const [LL, setLL] = useState<TF>(() =>
!locale ? getFallbackProxy<TF>() : i18nObject<L, T, TF, F>(locale, translations[locale], formatters[locale]),
)

const setLocale = useCallback((newLocale: L) => {
_setLocale(newLocale)
setLL(() => i18nObject<L, T, TF, F>(newLocale, translations[newLocale], formatters[newLocale]))
}, [])

const ctx = useMemo<I18nContextType<L, T, TF>>(
() => ({
setLocale,
locale,
LL,
}),
[setLocale, locale, LL],
)

return h(context.Provider, { value: ctx, children: props.children })
}

return { component, context }
}
7 changes: 7 additions & 0 deletions packages/adapter-preact/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../preact"
},
"include": ["src/**/*.ts"]
}
7 changes: 6 additions & 1 deletion packages/cli/src/setup/detect-setup.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const useAdapterWhenDependenciesContain =
shouldContain.reduce((prev, dep) => prev || dependencies.includes(dep), false as boolean)

const shouldUseAngularAdapter = useAdapterWhenDependenciesContain(['@angular/core'])
const shouldUsePreactAdapter = useAdapterWhenDependenciesContain(['preact'])
const shouldUseReactAdapter = useAdapterWhenDependenciesContain(['react', 'next'])
const shouldUseSolidAdapter = useAdapterWhenDependenciesContain(['solid-js'])
const shouldUseSvelteAdapter = useAdapterWhenDependenciesContain(['svelte', '@sveltejs/kit', 'sapper'])
Expand All @@ -21,6 +22,7 @@ const getAdaptersInfo = (type: RuntimeObject['type'], deps: string[]): Adapters[
const adapters: Adapters[] = []

if (shouldUseAngularAdapter(deps)) adapters.push('angular')
if (shouldUsePreactAdapter(deps)) adapters.push('preact')
if (shouldUseReactAdapter(deps)) adapters.push('react')
if (shouldUseSolidAdapter(deps)) adapters.push('solid')
if (shouldUseSvelteAdapter(deps)) adapters.push('svelte')
Expand All @@ -47,9 +49,12 @@ export const getDefaultConfig = async () => {
const esmImports = (await runtime.getEsmImportOption()) && !adapters.includes('svelte')

const defaultConfig = await getConfigWithDefaultValues()
const adapterConfig =
adapters.length === 1 ? ({ adapter: adapters[0] } as GeneratorConfig) : adapters.length > 1 ? ({ adapters } as GeneratorConfig) : {}

const config: GeneratorConfig = {
baseLocale: defaultConfig.baseLocale,
...(adapters ? (adapters.length === 1 ? { adapter: adapters[0] as Adapters } : { adapters }) : {}),
...adapterConfig,
esmImports,
outputFormat: isTypeScriptProject ? 'TypeScript' : 'JavaScript',
outputPath: defaultConfig.outputPath,
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/setup/questions.mts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const askConfigQuestions = ({ baseLocale, adapter, esmImports, outputForm
{ title: 'Angular', value: 'angular', description: 'this is an Angular application' },
{ title: 'Node.js', value: 'node', description: 'for Backends, APIs' },
{ title: 'React', value: 'react', description: 'this is a React/Next.js application' },
{ title: 'Preact', value: 'preact', description: 'this is a Preact application' },
{ title: 'Solid', value: 'solid', description: 'this is a SolidJS application' },
{ title: 'Svelte', value: 'svelte', description: 'this is a Svelte/SvelteKit/Sapper application' },
{ title: 'Vue.js', value: 'vue', description: 'this is a Vue.js application' },
Expand All @@ -30,10 +31,14 @@ export const askConfigQuestions = ({ baseLocale, adapter, esmImports, outputForm
return 2
case 'react':
return 3
case 'svelte':
case 'preact':
return 4
case 'vue':
case 'solid':
return 5
case 'svelte':
return 6
case 'vue':
return 7
default:
return 0
}
Expand Down
2 changes: 1 addition & 1 deletion packages/config/src/types.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Locale } from '../../runtime/src/core.mjs'

export type Adapters = 'angular' | 'deno' | 'node' | 'react' | 'solid' | 'svelte' | 'vue'
export type Adapters = 'angular' | 'deno' | 'node' | 'preact' | 'react' | 'solid' | 'svelte' | 'vue'

export type OutputFormats = 'TypeScript' | 'JavaScript'

Expand Down
1 change: 1 addition & 0 deletions packages/fix-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const folders = [
'runtime/esm/runtime/src',
'formatters',
'parser',
'preact',
'react',
'solid',
'svelte',
Expand Down
5 changes: 2 additions & 3 deletions packages/generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ The available options are:

| key | type | default value |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| [adapter](#adapter) | `'angular' \| 'node' \| 'react' \| 'solid' \| 'svelte' \| 'vue' \| undefined` | `undefined` |
| [adapters](#adapters) | `Array<'angular' \| 'node' \| 'react' \| 'solid' \| 'svelte' \| 'vue'> \| undefined` | `undefined`
| [adapter](#adapter) | `'angular' \| 'node' \| 'preact' \| 'react' \| 'solid' \| 'svelte' \| 'vue' \| undefined` | `undefined` |
| [adapters](#adapters) | `Array<'angular' \| 'node' \| 'preact' \| 'react' \| 'solid' \| 'svelte' \| 'vue'> \| undefined` | `undefined`
| [baseLocale](#baselocale) | `string` | `'en'` |
| [outputFormat](#outputformat) | `'TypeScript'` &#124; `'JavaScript'` | `'TypeScript'` |
| [esmImports](#esmimports) | `boolean | '.js' | 'fileEnding' | `false` |
Expand Down Expand Up @@ -358,4 +358,3 @@ const displaySettingsPage = async (locale) => {
}
```
> make sure to call `setLocale` after you load new namespaces !

Loading