From 18a11115ae7408177f104192d1321fa918024249 Mon Sep 17 00:00:00 2001 From: KishiTheMechanic Date: Sat, 26 Oct 2024 23:53:40 +0200 Subject: [PATCH] add locale. no need to choose namespace because already done --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++- deno.json | 2 +- src/i18nPlugin.ts | 34 +++++++++++++++++++++++++++++- src/types.ts | 10 ++++----- src/useTranslation.ts | 24 +++++++++++++++------- 5 files changed, 103 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3172408..26d4e39 100644 --- a/README.md +++ b/README.md @@ -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() @@ -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() + +// 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)` diff --git a/deno.json b/deno.json index 135740e..6ca44ba 100644 --- a/deno.json +++ b/deno.json @@ -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", diff --git a/src/i18nPlugin.ts b/src/i18nPlugin.ts index 8599d44..886157a 100644 --- a/src/i18nPlugin.ts +++ b/src/i18nPlugin.ts @@ -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> { const content = await Deno.readTextFile(filePath) try { @@ -17,10 +30,19 @@ async function readJsonFile(filePath: string): Promise> { } } +/** + * 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>; path: string } + { t: Record>; path: string; locale: string } > => { return async (ctx) => { const url = new URL(ctx.req.url) @@ -29,6 +51,7 @@ 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 @@ -36,8 +59,16 @@ export const i18nPlugin = ( ctx.state.path = rootPath pathname.value = rootPath + // Set the current locale in the state + ctx.state.locale = lang const translationDataSSR: Record> = {} + /** + * 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`) @@ -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) diff --git a/src/types.ts b/src/types.ts index 3ef02fc..2087600 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 { req: Request state: State - next: () => Promise // `next`を追加 + next: () => Promise } /** @@ -27,12 +25,14 @@ export type MiddlewareFn = ( ) => Promise /** - * 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> path: string + locale: string } diff --git a/src/useTranslation.ts b/src/useTranslation.ts index 7c7e5fc..dd4c7f4 100644 --- a/src/useTranslation.ts +++ b/src/useTranslation.ts @@ -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)[k] + } else { + return key // Fallback to the key if the path is not found + } + } + + return typeof value === 'string' ? value : key } return { t: translate }