Skip to content

Commit

Permalink
testing
Browse files Browse the repository at this point in the history
  • Loading branch information
KishiTheMechanic committed Oct 27, 2024
1 parent 12591b6 commit e5d7566
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 199 deletions.
186 changes: 139 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ directly via ctx.state.t, enabling SSR with localized data.

```typescript
import { createDefine } from 'fresh'
import type { TranslationState } from '@elsoul/fresh-i18n'
import type { TranslationState } from 'fresh-i18n'

interface State {
title?: string
Expand All @@ -97,10 +97,8 @@ interface State {
noIndex?: boolean
}

// Combine TranslationState with custom State properties
export type ExtendedState = State & TranslationState

// Define the extended state for use in your Fresh app
export const define = createDefine<ExtendedState>()
```

Expand All @@ -114,6 +112,8 @@ For example, if the URL is `https://example.com/en/company/profile`, the plugin
will load the following files (if they exist):

- `./locales/en/common.json` (always loaded as the base translation)
- `./locales/en/metadata.json` (always loaded as the base translation)
- `./locales/en/error.json` (always loaded as the base translation)
- `./locales/en/company.json`
- `./locales/en/profile.json`

Expand All @@ -138,78 +138,170 @@ file does not exist, it is skipped without an error, ensuring flexibility.
}
```

### Step 3: Use Translations in Components

Leverage `useTranslation()` and `useLocale()` hooks in components to access
translations and handle language switching dynamically.
### Step 3: Use Translations in Routes

```tsx
import { useLocale, useTranslation } from '@elsoul/fresh-i18n'
import { define } from '@/utils/state.ts'

export default function IslandsComponent() {
const { t } = useTranslation()
const { locale, changeLanguage } = useLocale()
export const handler = define.handlers({
GET(ctx) {
console.log('ctx', ctx.state.translationData) // Access translation data directly
return page()
},
})

export default define.page<typeof handler>(function Home(props) {
console.log('props', props.state.t('common.title')) // Access translation data using getTranslation function via props
return (
<div>
<h1>{t('common.title')}</h1> {/* Outputs "Home" or "ホーム" */}
<p>{t('common.welcome')}</p> {/* Outputs "Welcome" or "ようこそ" */}
<p>Current language: {locale}</p>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('ja')}>日本語</button>
</div>
)
})
```

### Step 4: Use Translation in Islands

You need to share ctx.state data with islands.

```tsx:./routes/_layouts.tsx
import type { PageProps } from 'fresh'
import StateShareLayer from '@/islands/layouts/StateShareLayer.tsx'
import type { ExtendedState } from '@/utils/state.ts'

export default function RootLayout(
{ Component, state }: PageProps,
) {
return (
<>
<StateShareLayer state={state as ExtendedState} />
<Component />
</>
)
}
```

```tsx
// Example usage in a route handler for SSR
export const handler = define.handlers({
GET(ctx) {
console.log('ctx', ctx.state.t) // Access translation data directly
return page()
},
```tsx:./islands/layouts/StateShareLayer.tsx
import { type ExtendedState } from '@/utils/state.ts'
import { atom, useAtom } from 'fresh-atom'
import { useEffect } from 'preact/hooks'

type Props = {
state: ExtendedState
}

export const stateAtom = atom<ExtendedState>({
title: '',
theme: 'dark',
description: '',
ogImage: '',
noIndex: false,
locale: 'en',
t: {},
path: '/',
})

export default function StateShareLayer({ state }: Props) {
const [, setState] = useAtom(stateAtom)

useEffect(() => {
setState(state)
}, [state])

return null
}
```

### API Reference
#### Useful hooks

You can create useful hooks to access translation data on islands.

#### `i18nPlugin(options)`
```tsx:./hooks/i18n/useTranslation.ts
import { useAtom } from 'fresh-atom'
import { stateAtom } from '@/islands/layouts/StateShareLayer.tsx'

Registers the i18n middleware for handling translation loading and locale
management.
export function useTranslation() {
const [state] = useAtom(stateAtom)

- **Options**:
- `languages` (string[]): An array of supported languages (e.g.,
`['en', 'ja']`).
- `defaultLanguage` (string): The default language code, used if no locale is
detected.
- `localesDir` (string): Path to the directory containing locale files.
/**
* Translates a key string like 'common.title' or 'common.titlerow.title.example'
* by traversing the nested structure of `state.t`.
*
* @param key - The translation key in dot notation (e.g., 'common.title').
* @returns The translated string, or an empty string if the key is not found.
*/
const t = (key: string): string => {
const keys = key.split('.')
let result: Record<string, unknown> | string = state.t

#### `useTranslation(namespace: string)`
for (const k of keys) {
if (typeof result === 'object' && result !== null && k in result) {
result = result[k] as Record<string, unknown> | string
} else {
return '' // Key not found, return empty string or default text
}
}

Hook to access translation strings within a specified namespace.
return typeof result === 'string' ? result : '' // Return the result if it's a string
}

- **Parameters**:
- `namespace` (string): Namespace identifier to load relevant translations.
return { t }
}
```

```tsx:./hooks/i18n/usePathname.ts
import { useAtom } from 'fresh-atom'
import { stateAtom } from '@/islands/layouts/StateShareLayer.tsx'

#### `useLocale()`
export function usePathname() {
const [state] = useAtom(stateAtom)
return state.path
}
```

Hook to retrieve and change the current locale.
```tsx:./hooks/i18n/useLocale.ts
import { useAtom } from 'fresh-atom'
import { stateAtom } from '@/islands/layouts/StateShareLayer.tsx'

- **Returns**:
- `locale` (string): Current locale code.
- `changeLanguage` (function): Function to update the locale.
export function useLocale() {
const [state, setState] = useAtom(stateAtom)

#### `Link` Component
/**
* Sets a new locale, updates the global state, and redirects
* to the new locale's URL path to update page content.
*
* @param locale - The new locale string (e.g., 'en', 'ja').
*/
const setLocale = (locale: string) => {
setState((prevState) => ({ ...prevState, locale }))

A custom `Link` component that maintains the current locale in app-internal
links for consistent navigation.
const newPath = `/${locale}${state.path}`
globalThis.location.href = newPath
}

return { locale: state.locale, setLocale }
}
```

##### Usage

```tsx
import { Link } from '@elsoul/fresh-i18n';
import { useTranslation } from '@/hooks/i18n/useTranslation.ts'
import { usePathname } from '@/hooks/i18n/usePathname.ts'
import { useLocale } from '@/hooks/i18n/useLocale.ts'

export default function IslandsComponent() {
const { t } = useTranslation()
const path = usePathname()
const { locale } = useLocale()

<Link href="/about">About Us</Link> {/* Locale-aware navigation */}
console.log('path', path)
console.log('locale', locale)
return (
<div>
{t('common.title')} // Home or ホーム
</div>
)
}
```

## Contributing
Expand Down
13 changes: 1 addition & 12 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@elsoul/fresh-i18n",
"version": "0.9.9",
"version": "0.9.10",
"description": "A simple and flexible internationalization (i18n) plugin for Deno's Fresh framework.",
"runtimes": ["deno", "browser"],
"exports": "./mod.ts",
Expand All @@ -11,23 +11,12 @@
"license": "Apache-2.0",
"imports": {
"@/": "./",
"fresh": "jsr:@fresh/[email protected]",
"fresh-atom": "jsr:@elsoul/[email protected]",
"preact": "npm:[email protected]",
"preact/hooks": "npm:[email protected]/hooks",
"@preact/signals": "npm:@preact/[email protected]",
"@preact/signals-core": "npm:@preact/[email protected]",
"@std/expect": "jsr:@std/[email protected]",
"@std/path": "jsr:@std/[email protected]"
},
"fmt": {
"options": {
"semiColons": false,
"singleQuote": true
}
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
4 changes: 0 additions & 4 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
export { i18nPlugin } from '@/src/i18nPlugin.ts'
export { useTranslation } from '@/src/useTranslation.ts'
export { useLocale } from '@/src/useLocale.ts'
export { usePathname } from '@/src/usePathname.ts'
export { Link } from './src/Link.tsx'

export type { TranslationState } from '@/src/types.ts'
30 changes: 0 additions & 30 deletions src/Link.tsx

This file was deleted.

41 changes: 32 additions & 9 deletions src/i18nPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { join } from '@std/path'
import { currentLocale, pathname, translationData } from '@/src/store.ts'
import type { MiddlewareFn, TranslationState } from '@/src/types.ts'

/**
Expand Down Expand Up @@ -30,6 +29,31 @@ async function readJsonFile(filePath: string): Promise<Record<string, string>> {
}
}

/**
* Retrieves a translation value from a nested translation object.
*
* @param translations - The translations object (e.g., ctx.state.translationData).
* @param key - The translation key in dot notation (e.g., 'common.title').
* @returns The translated string, or an empty string if the key is not found.
*/
function getTranslation(
translations: Record<string, unknown>,
key: string,
): string {
const keys = key.split('.')
let result: unknown = translations

for (const k of keys) {
if (typeof result === 'object' && result !== null && k in result) {
result = (result as Record<string, unknown>)[k]
} else {
return '' // Return empty string if key is not found
}
}

return typeof result === 'string' ? result : ''
}

/**
* Middleware function to initialize internationalization (i18n) support.
* This plugin detects the user's language based on the URL, loads the necessary
Expand All @@ -56,10 +80,7 @@ export const i18nPlugin = (
ctx.state.path = rootPath
ctx.state.locale = lang

pathname.set(rootPath)
currentLocale.set(lang)

const translationDataSSR: Record<string, Record<string, string>> = {}
const translationData: Record<string, Record<string, string>> = {}

/**
* Loads a translation namespace by reading the corresponding JSON file from `localesDir`.
Expand All @@ -71,7 +92,7 @@ export const i18nPlugin = (
const filePath = join(localesDir, lang, `${namespace}.json`)
const data = await readJsonFile(filePath)
if (Object.keys(data).length > 0) {
translationDataSSR[namespace] = data
translationData[namespace] = data
}
}

Expand All @@ -84,10 +105,12 @@ export const i18nPlugin = (
await loadTranslation(segment)
}

ctx.state.t = translationDataSSR
translationData.set(translationDataSSR)
// Set translationData and t function in ctx.state
ctx.state.translationData = translationData
ctx.state.t = (key: string) =>
getTranslation(ctx.state.translationData, key)

const response = await ctx.next() as Response
return response ?? new Response(null, { status: 204 })
return response
}
}
Loading

0 comments on commit e5d7566

Please sign in to comment.