Skip to content

Commit

Permalink
add locale. no need to choose namespace because already done
Browse files Browse the repository at this point in the history
  • Loading branch information
KishiTheMechanic committed Oct 26, 2024
1 parent e82996f commit 18a1111
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 15 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ translations and handle language switching dynamically.
```tsx
import { useLocale, useTranslation } from '@elsoul/fresh-i18n'

export default function Home() {
export default function IslandsComponent() {
const { t } = useTranslation('common') // Uses "common" namespace
const { locale, changeLanguage } = useLocale()

Expand All @@ -124,6 +124,52 @@ export default function Home() {
}
```

### Define an Extended State with TranslationState

If you are managing additional global state in your Fresh app, such as metadata
or theme settings, you can extend TranslationState to include your own
properties. This extended state can then be used across your app, with
translation data (t) accessible directly in request handlers, enabling
Server-Side Rendering (SSR) with fully localized content.

#### Example

In the following example, TranslationState from @elsoul/fresh-i18n is combined
with a custom State interface to create ExtendedState. This ExtendedState
includes both translation data and other application-specific properties, making
it convenient for global state management.

ExtendedState can then be used in request handlers to access translation data
directly via ctx.state.t, enabling SSR with localized data.

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

interface State {
title?: string
lang?: string
theme?: string
description?: string
ogImage?: string
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>()

// Example usage in a route handler
export const handler = define.handlers({
GET(ctx) {
console.log('ctx', ctx.state.t) // Access translation data directly
return page()
},
})
```

### API Reference

#### `i18nPlugin(options)`
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@elsoul/fresh-i18n",
"version": "0.7.1",
"version": "0.8.0",
"description": "A simple and flexible internationalization (i18n) plugin for Deno's Fresh framework.",
"runtimes": ["deno", "browser"],
"exports": "./mod.ts",
Expand Down
34 changes: 33 additions & 1 deletion src/i18nPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,25 @@ import { join } from '@std/path'
import { pathname, translationData } from '@/src/store.ts'
import type { MiddlewareFn } from '@/src/types.ts'

/**
* Configuration options for the i18n plugin.
*
* @property languages - Array of supported language codes (e.g., ['en', 'ja']).
* @property defaultLanguage - Default language code used when no language is detected.
* @property localesDir - Directory path where translation JSON files are stored.
*/
export interface I18nOptions {
languages: string[]
defaultLanguage: string
localesDir: string
}

/**
* Reads a JSON file and parses its contents.
*
* @param filePath - Path to the JSON file.
* @returns Parsed JSON object as a record of key-value pairs.
*/
async function readJsonFile(filePath: string): Promise<Record<string, string>> {
const content = await Deno.readTextFile(filePath)
try {
Expand All @@ -17,10 +30,19 @@ async function readJsonFile(filePath: string): Promise<Record<string, string>> {
}
}

/**
* Middleware function to initialize internationalization (i18n) support.
* This plugin detects the user's language based on the URL, loads the necessary
* translation files dynamically, and saves the translations, locale, and base path as
* global signals for both client-side and server-side access.
*
* @param options - Configuration options for the i18n plugin.
* @returns A middleware function that handles language detection and translation loading.
*/
export const i18nPlugin = (
{ languages, defaultLanguage, localesDir }: I18nOptions,
): MiddlewareFn<
{ t: Record<string, Record<string, string>>; path: string }
{ t: Record<string, Record<string, string>>; path: string; locale: string }
> => {
return async (ctx) => {
const url = new URL(ctx.req.url)
Expand All @@ -29,15 +51,24 @@ export const i18nPlugin = (
? pathSegments[0]
: defaultLanguage

// Sets the root path without the language prefix for client-side navigation.
const rootPath = lang === pathSegments[0]
? '/' + pathSegments.slice(1).join('/')
: url.pathname

ctx.state.path = rootPath
pathname.value = rootPath

// Set the current locale in the state
ctx.state.locale = lang
const translationDataSSR: Record<string, Record<string, string>> = {}

/**
* Loads a translation namespace by reading the corresponding JSON file from `localesDir`.
* If the file does not exist, it is ignored.
*
* @param namespace - The namespace of the translation file to load (e.g., 'common').
*/
const loadTranslation = async (namespace: string) => {
try {
const filePath = join(localesDir, lang, `${namespace}.json`)
Expand All @@ -48,6 +79,7 @@ export const i18nPlugin = (
}
}

// Load the common namespace and additional namespaces based on the URL path.
await loadTranslation('common')
for (
const segment of pathSegments.slice(lang === pathSegments[0] ? 1 : 0)
Expand Down
10 changes: 5 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
// types.ts

/**
* Represents the context passed to every middleware function.
*
* @template State - The type of state held in the context.
* @property req - The original incoming `Request` object.
* @property state - The current translation and path state.
* @property state - The current translation, path, and locale state.
* @property next - Function to invoke the next middleware in the chain.
*/
export interface FreshContext<State> {
req: Request
state: State
next: () => Promise<Response | void> // `next`を追加
next: () => Promise<Response | void>
}

/**
Expand All @@ -27,12 +25,14 @@ export type MiddlewareFn<State = TranslationState> = (
) => Promise<Response | void>

/**
* Represents the state of translations and the base path within the app.
* Represents the state of translations, the base path, and locale within the app.
*
* @property t - Object holding translation data for different namespaces.
* @property path - The base path of the URL without the language prefix.
* @property locale - The current locale code, used for translations.
*/
export interface TranslationState {
t: Record<string, Record<string, string>>
path: string
locale: string
}
24 changes: 17 additions & 7 deletions src/useTranslation.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
// useTranslation.ts

import { translationData } from '@/src/store.ts'

/**
* Provides access to translation strings within a specified namespace.
* Provides access to translation strings with support for deeply nested keys.
*
* @param namespace - The namespace of translations to retrieve (e.g., 'common', 'company').
* @returns An object containing a function to fetch translations by key within the given namespace.
* @returns An object containing a function to fetch translations by deeply nested key.
*/
export function useTranslation(
namespace: string,
): { t: (key: string) => string } {
export function useTranslation(): { t: (key: string) => string } {
const translate = (key: string): string => {
return translationData.value[namespace]?.[key] ?? key
const keys = key.split('.')
let value: unknown = translationData.value

for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = (value as Record<string, unknown>)[k]
} else {
return key // Fallback to the key if the path is not found
}
}

return typeof value === 'string' ? value : key
}

return { t: translate }
Expand Down

0 comments on commit 18a1111

Please sign in to comment.