From 231d73064cb45f159aad3ac5ebf6df7e655fff4d Mon Sep 17 00:00:00 2001 From: melvinchia3636 Date: Wed, 28 Jan 2026 14:12:45 +0800 Subject: [PATCH] feat: migrate locale files to a new `@lifeforge/--lang-` package structure and refactor locale validation --- .gitignore | 2 +- .../02.user-guide/06.LanguagePacks.mdx | 40 ++--- .../03.developer-guide/04.Localization.mdx | 163 ++++++++++-------- .../accountSettings.json | 0 .../{en => lifeforge--lang-en}/apiKeys.json | 0 locales/{en => lifeforge--lang-en}/auth.json | 0 .../{en => lifeforge--lang-en}/backups.json | 0 .../{en => lifeforge--lang-en}/buttons.json | 0 .../{en => lifeforge--lang-en}/dashboard.json | 0 .../documentation.json | 0 locales/{en => lifeforge--lang-en}/fetch.json | 0 locales/{en => lifeforge--lang-en}/misc.json | 0 .../{en => lifeforge--lang-en}/modals.json | 0 .../moduleManager.json | 0 .../{en => lifeforge--lang-en}/package.json | 0 .../personalization.json | 0 .../{en => lifeforge--lang-en}/sidebar.json | 0 locales/{en => lifeforge--lang-en}/vault.json | 0 .../locales/handlers/publishLocaleHandler.ts | 4 +- .../validateLocaleStructure.ts | 37 ++-- tools/src/commands/locales/index.ts | 7 + 21 files changed, 147 insertions(+), 106 deletions(-) rename locales/{en => lifeforge--lang-en}/accountSettings.json (100%) rename locales/{en => lifeforge--lang-en}/apiKeys.json (100%) rename locales/{en => lifeforge--lang-en}/auth.json (100%) rename locales/{en => lifeforge--lang-en}/backups.json (100%) rename locales/{en => lifeforge--lang-en}/buttons.json (100%) rename locales/{en => lifeforge--lang-en}/dashboard.json (100%) rename locales/{en => lifeforge--lang-en}/documentation.json (100%) rename locales/{en => lifeforge--lang-en}/fetch.json (100%) rename locales/{en => lifeforge--lang-en}/misc.json (100%) rename locales/{en => lifeforge--lang-en}/modals.json (100%) rename locales/{en => lifeforge--lang-en}/moduleManager.json (100%) rename locales/{en => lifeforge--lang-en}/package.json (100%) rename locales/{en => lifeforge--lang-en}/personalization.json (100%) rename locales/{en => lifeforge--lang-en}/sidebar.json (100%) rename locales/{en => lifeforge--lang-en}/vault.json (100%) rename tools/src/commands/locales/{functions => handlers}/validateLocaleStructure.ts (64%) diff --git a/.gitignore b/.gitignore index 8a48d486a..e83c666e8 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,4 @@ keys # user-generated files apps locales/* -!locales/en \ No newline at end of file +!locales/lifeforge--lang-en \ No newline at end of file diff --git a/docs/src/contents/02.user-guide/06.LanguagePacks.mdx b/docs/src/contents/02.user-guide/06.LanguagePacks.mdx index a87abe439..40df6ea21 100644 --- a/docs/src/contents/02.user-guide/06.LanguagePacks.mdx +++ b/docs/src/contents/02.user-guide/06.LanguagePacks.mdx @@ -26,6 +26,7 @@ The `ForgeCLI` provides commands to manage language packs: | `bun forge locales list` | List all installed language packs | | `bun forge locales add ` | Install a new language pack | | `bun forge locales remove ` | Remove an installed language pack | +| `bun forge locales validate ` | Validate a language pack structure | @@ -53,26 +54,26 @@ Installed language packs (4):
## Installing a Language Pack -To install a new language pack, use the `add` command with the language code: +To install a new language pack, use the `add` command with the package name: ```bash -bun forge locales add +bun forge locales add ``` For example, to install Japanese: ```bash -bun forge locales add ja +bun forge locales add lifeforge--lang-ja ``` -Language packs are fetched from the official LifeForge repository at `lifeforge-app/lang-`. Make sure the language pack exists before attempting to install it. +Language packs are fetched from the official LifeForge registry. The package name follows the format `--lang-`. ### What Happens During Installation -1. **Git Submodule** — The language pack is cloned as a git submodule into `locales/` -2. **Structure Validation** — The CLI verifies that `manifest.json` exists +1. **Package Installation** — The language pack is downloaded into `locales/` +2. **Structure Validation** — The CLI verifies that `package.json` is valid 3. **Database Update** — If this is the first language being installed, all users' language preferences are automatically set to this language @@ -87,20 +88,19 @@ After installing a language pack, restart the server for the changes to take eff To remove an installed language pack: ```bash -bun forge locales remove +bun forge locales remove ``` For example, to remove Simplified Chinese: ```bash -bun forge locales remove zh-CN +bun forge locales remove melvinchia3636--lang-zh-CN ``` ### What Happens During Removal 1. **User Migration** — Any users with the removed language as their preference are automatically switched to the first remaining language -2. **Git Submodule Cleanup** — The submodule is deinitialized and removed from `.gitmodules` -3. **Directory Removal** — The language pack directory is deleted +2. **Directory Removal** — The language pack directory is deleted You cannot remove the last remaining language pack. At least one language must always be installed. @@ -117,13 +117,13 @@ The removal process requires database access. If PocketBase is not running, a te The following official language packs are available: -| Code | Language | Repository | Author / Contributors | -|------|----------|------------|--------| -| `en` | English | [lifeforge-app/lang-en](https://github.com/lifeforge-app/lang-en) | | -| `ms` | Bahasa Malaysia | [lifeforge-app/lang-ms](https://github.com/lifeforge-app/lang-ms) | | -| `zh-CN` | 简体中文 | [lifeforge-app/lang-zh-CN](https://github.com/lifeforge-app/lang-zh-CN) | | -| `zh-TW` | 繁體中文 | [lifeforge-app/lang-zh-TW](https://github.com/lifeforge-app/lang-zh-TW) | | -| `tr` | Türkçe | [lifeforge-app/lang-tr](https://github.com/lifeforge-app/lang-tr) | | +| Package Name | Language | Repository | Author / Contributors | +|--------------|----------|------------|--------| +| `lifeforge--lang-en` | English | [lifeforge-app/lang-en](https://github.com/lifeforge-app/lang-en) | | +| `melvinchia3636--lang-ms` | Bahasa Malaysia | [lifeforge-app/lang-ms](https://github.com/lifeforge-app/lang-ms) | | +| `melvinchia3636--lang-zh-CN` | 简体中文 | [lifeforge-app/lang-zh-CN](https://github.com/lifeforge-app/lang-zh-CN) | | +| `melvinchia3636--lang-zh-TW` | 繁體中文 | [lifeforge-app/lang-zh-TW](https://github.com/lifeforge-app/lang-zh-TW) | | +| `lifeforge--lang-tr` | Türkçe | [lifeforge-app/lang-tr](https://github.com/lifeforge-app/lang-tr) | | Want to contribute a new language? See the [Creating Language Packs](/developer-guide/localization#creating-language-packs) section in the Developer Guide. @@ -137,7 +137,7 @@ Want to contribute a new language? See the [Creating Language Packs](/developer- ### Language Pack Not Loading After Installation - Restart the development server (`bun forge dev`) -- Verify the `manifest.json` file exists and is valid JSON +- Verify the `package.json` file exists and is valid - Check server logs for any error messages ### "Language is already installed" Error @@ -145,8 +145,8 @@ Want to contribute a new language? See the [Creating Language Packs](/developer- The language pack directory already exists. If you want to reinstall: ```bash -bun forge locales remove -bun forge locales add +bun forge locales remove +bun forge locales add ``` ### Git Submodule Errors diff --git a/docs/src/contents/03.developer-guide/04.Localization.mdx b/docs/src/contents/03.developer-guide/04.Localization.mdx index 79cf888a0..3aaafc476 100644 --- a/docs/src/contents/03.developer-guide/04.Localization.mdx +++ b/docs/src/contents/03.developer-guide/04.Localization.mdx @@ -26,29 +26,20 @@ LifeForge uses **[i18next](https://www.i18next.com/)** with **react-i18next** fo -
- **4 Supported Languages** (for now) + **Community Driven Language Support**
- English (`en`), Simplified Chinese (`zh-CN`), Traditional Chinese (`zh-TW`), and Bahasa Malaysia (`ms`). + LifeForge is translated by the community. Anyone can contribute a new language pack or improve existing translations. -
- **Dynamic Loading** + **Package-based Architecture**
- Translations are fetched on-demand via HTTP backend, reducing initial bundle size + Translations are managed as NPM packages, allowing for easy updates and versioning. -
**Namespace Architecture**
Modular organization with `apps.\*` and `common.*` namespaces for clean separation --
- - **UI-based Management** -
- Dedicated Localization Manager tool for easy editing and maintenance --
- - **AI Translation Assistance** -
- Built-in Gemini API integration for translation suggestions in the Localization Manager tool + -
**Missing Key Reporting** @@ -62,13 +53,13 @@ LifeForge uses **[i18next](https://www.i18next.com/)** with **react-i18next** fo ### Module Locales (apps.* namespace) -Each module has its own `locales/` directory containing 4 JSON files, one for each supported language: +Each module has its own `locales/` directory containing a single JSON file for each supported language: ```plaintext apps/ -└── myModule/ +└── melvinchia3636--my-module/ └── locales/ - ├── en.json # English (primary) + ├── en.json # English ├── ms.json # Bahasa Malaysia ├── zh-CN.json # Simplified Chinese └── zh-TW.json # Traditional Chinese @@ -76,21 +67,16 @@ apps/ ### Core Locales (common.* namespace) -Shared translations used across the entire application are stored in the server's core locales: +Shared translations used across the entire application are stored in the language pack packages within the `locales/` directory. Each package contains multiple JSON files representing different namespaces: ```plaintext -server/src/core/locales/ -├── en/ -│ ├── auth.json # Authentication strings -│ ├── buttons.json # Common button labels -│ ├── fetch.json # Loading/fetch states -│ ├── misc.json # Miscellaneous strings -│ ├── modals.json # Common modal strings -│ ├── sidebar.json # Sidebar navigation -│ └── vault.json # Master password vault -├── ms/ -├── zh-CN/ -└── zh-TW/ +locales/ +└── lifeforge--lang-en/ + ├── package.json # Manifest + ├── auth.json # common.auth + ├── buttons.json # common.buttons + ├── fetch.json # common.fetch + ├── ... ```
@@ -266,35 +252,7 @@ When using `ForgeCLI` to create a module, locale files are automatically generat -
-## Localization Manager -LifeForge includes a dedicated web-based **Localization Manager** tool for managing translations across your entire application: - --
- - **Visual Tree Editor** -
- Navigate and edit locale keys in a hierarchical tree structure --
- - **Side-by-Side Editing** -
- Edit translations for all languages simultaneously --
- - **AI Translation Suggestions** -
- Get translation suggestions powered by Gemini API --
- - **Search Functionality** -
- Quickly find keys across all namespaces - -The Localization Manager is accessible via SSO from the main application and is located in the `tools/localizationManager` directory. - -
## Best Practices @@ -373,27 +331,90 @@ When adding a key to English, immediately add placeholder or translated values t
## Creating Language Packs -Want to contribute a new language to LifeForge? Here's how to create a language pack: +Want to contribute a new language to LifeForge? The localization system is built on top of NPM packages, making it easy to create, share, and manage language packs. -1. **Fork an existing language pack** — Start with the English ([lang-en](https://github.com/lifeforge-app/lang-en)) repository as a template -2. **Translate all JSON files** — Maintain the same structure and keys -3. **Update `manifest.json`** — Set the correct `name`, `icon`, and `displayName` -4. **Create the repository** — Name it `lang-` (e.g., `lang-ja` for Japanese) -5. **Submit for inclusion** — Open an issue in the main LifeForge repository +### 1. Creation Workflow -### Manifest File Structure +1. **Preparation** — Decide on your language and region code (e.g., `es-ES` for Spanish (Spain)). +2. **Naming Convention** — Packages must follow the format `@lifeforge/--lang-`: + * ``: Your GitHub username or organization name (e.g., `melvinchia3636`) + * ``: The language code (e.g., `ms`, `zh-CN`) + * Example: `@lifeforge/melvinchia3636--lang-ms` +3. **Directory Structure** — Create a directory in `locales/` matching your package name sans scope: + * Directory: `locales/melvinchia3636--lang-ms` + +### 2. Package Structure + +Every language pack MUST contain a `package.json` file and a set of JSON translation files. + +#### package.json Configuration + +The `package.json` acts as the manifest for your language pack. It is strictly validated using Zod schemas. ```json { - "name": "ja", - "icon": "circle-flags:jp", - "displayName": "日本語" + "name": "@lifeforge/melvinchia3636--lang-ms", + "version": "0.0.1", + "description": "Bahasa Malaysia language support for LifeForge.", + "author": "melvinchia3636 ", + "lifeforge": { + "code": "ms", + "displayName": "Bahasa Malaysia", + "icon": "circle-flags:my", + "alternative": ["Bahasa Melayu", "Malay"] + } } ``` - -The `icon` field uses [Iconify](https://icon-sets.iconify.design/) icon names. The `circle-flags` set is recommended for country flags. - +**Required Fields:** + +| Field | Description | Validation Rule | +|-------|-------------|-----------------| +| `name` | Package name | Must match regex `/^@lifeforge\/.+--lang-.+$/` | +| `version` | Semver version | Must match regex `/^\d+\.\d+\.\d+$/` | +| `lifeforge.code` | Language code | Standard language code | +| `lifeforge.displayName` | Human-readable name | Displayed in UI | +| `lifeforge.icon` | Iconify icon name | e.g. `circle-flags:my` | + +**Optional Fields:** + +| Field | Description | +|-------|-------------| +| `lifeforge.alternative` | Array of alternative names | Used for search aliases | +| `repository` | Git repository info | Standard `package.json` format | + +#### Translation Files + +All other files in the directory must be flat `.json` files corresponding to namespaces (e.g., `auth.json`, `buttons.json`). Use the official English language pack (`lifeforge--lang-en`) as the source of truth for the latest keys and structure. + +* **Allowed:** `.json` files, `package.json`, `.git` directory +* **Prohibited:** Nested directories, or any non-JSON files + +### 3. Validation + +The `ForgeCLI` enforces strict validation rules. Run the following to validate your structure: + +```bash +bun forge locales validate +``` + +If your locale appears in the list without errors, it is valid. Common validation errors include: +* Folder name mismatch with `package.name` +* Presence of non-JSON files +* Invalid JSON syntax +* Missing required `lifeforge` metadata in `package.json` + +### 4. Publishing + +Once your language pack is ready, you can publish it to the LifeForge registry. + +```bash +bun forge locales publish +``` + +* **Package Structure Validation**: The CLI will validate your package structure to make sure it is valid. +* **Automatic Versioning**: The CLI will automatically bump the patch version in `package.json`. +* **Author Validation**: The CLI verifies that you are the author defined in `package.json`.
diff --git a/locales/en/accountSettings.json b/locales/lifeforge--lang-en/accountSettings.json similarity index 100% rename from locales/en/accountSettings.json rename to locales/lifeforge--lang-en/accountSettings.json diff --git a/locales/en/apiKeys.json b/locales/lifeforge--lang-en/apiKeys.json similarity index 100% rename from locales/en/apiKeys.json rename to locales/lifeforge--lang-en/apiKeys.json diff --git a/locales/en/auth.json b/locales/lifeforge--lang-en/auth.json similarity index 100% rename from locales/en/auth.json rename to locales/lifeforge--lang-en/auth.json diff --git a/locales/en/backups.json b/locales/lifeforge--lang-en/backups.json similarity index 100% rename from locales/en/backups.json rename to locales/lifeforge--lang-en/backups.json diff --git a/locales/en/buttons.json b/locales/lifeforge--lang-en/buttons.json similarity index 100% rename from locales/en/buttons.json rename to locales/lifeforge--lang-en/buttons.json diff --git a/locales/en/dashboard.json b/locales/lifeforge--lang-en/dashboard.json similarity index 100% rename from locales/en/dashboard.json rename to locales/lifeforge--lang-en/dashboard.json diff --git a/locales/en/documentation.json b/locales/lifeforge--lang-en/documentation.json similarity index 100% rename from locales/en/documentation.json rename to locales/lifeforge--lang-en/documentation.json diff --git a/locales/en/fetch.json b/locales/lifeforge--lang-en/fetch.json similarity index 100% rename from locales/en/fetch.json rename to locales/lifeforge--lang-en/fetch.json diff --git a/locales/en/misc.json b/locales/lifeforge--lang-en/misc.json similarity index 100% rename from locales/en/misc.json rename to locales/lifeforge--lang-en/misc.json diff --git a/locales/en/modals.json b/locales/lifeforge--lang-en/modals.json similarity index 100% rename from locales/en/modals.json rename to locales/lifeforge--lang-en/modals.json diff --git a/locales/en/moduleManager.json b/locales/lifeforge--lang-en/moduleManager.json similarity index 100% rename from locales/en/moduleManager.json rename to locales/lifeforge--lang-en/moduleManager.json diff --git a/locales/en/package.json b/locales/lifeforge--lang-en/package.json similarity index 100% rename from locales/en/package.json rename to locales/lifeforge--lang-en/package.json diff --git a/locales/en/personalization.json b/locales/lifeforge--lang-en/personalization.json similarity index 100% rename from locales/en/personalization.json rename to locales/lifeforge--lang-en/personalization.json diff --git a/locales/en/sidebar.json b/locales/lifeforge--lang-en/sidebar.json similarity index 100% rename from locales/en/sidebar.json rename to locales/lifeforge--lang-en/sidebar.json diff --git a/locales/en/vault.json b/locales/lifeforge--lang-en/vault.json similarity index 100% rename from locales/en/vault.json rename to locales/lifeforge--lang-en/vault.json diff --git a/tools/src/commands/locales/handlers/publishLocaleHandler.ts b/tools/src/commands/locales/handlers/publishLocaleHandler.ts index 4c43c970a..583f9f63c 100644 --- a/tools/src/commands/locales/handlers/publishLocaleHandler.ts +++ b/tools/src/commands/locales/handlers/publishLocaleHandler.ts @@ -9,8 +9,8 @@ import logger from '@/utils/logger' import normalizePackage from '@/utils/normalizePackage' import { getRegistryUrl } from '../../../utils/registry' -import { validateLocaleStructure } from '../functions/validateLocaleStructure' import validateLocalesAuthor from '../functions/validateLocalesAuthor' +import { validateLocaleStructureHandler } from './validateLocaleStructure' export async function publishLocaleHandler(langCode: string): Promise { const { fullName, targetDir } = normalizePackage(langCode, 'locale') @@ -25,7 +25,7 @@ export async function publishLocaleHandler(langCode: string): Promise { } logger.info('Validating locale structure...') - validateLocaleStructure(targetDir) + validateLocaleStructureHandler({ lang: langCode }) logger.debug('Validating module author...') await validateLocalesAuthor(targetDir) diff --git a/tools/src/commands/locales/functions/validateLocaleStructure.ts b/tools/src/commands/locales/handlers/validateLocaleStructure.ts similarity index 64% rename from tools/src/commands/locales/functions/validateLocaleStructure.ts rename to tools/src/commands/locales/handlers/validateLocaleStructure.ts index 5b1beeedd..2bcd73f75 100644 --- a/tools/src/commands/locales/functions/validateLocaleStructure.ts +++ b/tools/src/commands/locales/handlers/validateLocaleStructure.ts @@ -1,8 +1,10 @@ +import chalk from 'chalk' import fs from 'fs' import path from 'path' import z from 'zod' import logger from '@/utils/logger' +import normalizePackage from '@/utils/normalizePackage' const localesPackageJSONSchema = z.object({ name: z.string().regex(/^@lifeforge\/.+--lang-.+$/), @@ -23,12 +25,23 @@ const localesPackageJSONSchema = z.object({ }) }) -export function validateLocaleStructure(localePath: string) { - const packageJsonPath = path.join(localePath, 'package.json') +export function validateLocaleStructureHandler(lang: string) { + const { targetDir } = normalizePackage(lang, 'locale') + + if (!fs.existsSync(targetDir)) { + logger.actionableError( + `Locale "${lang}" not found in locales/`, + 'Run "bun forge locales list" to see available locales' + ) + + process.exit(1) + } + + const packageJsonPath = path.join(targetDir, 'package.json') if (!fs.existsSync(packageJsonPath)) { logger.actionableError( - `Locale "${localePath}" is missing package.json`, + `Locale "${lang}" is missing package.json`, 'Run "bun forge locales list" to see available locales' ) @@ -49,11 +62,9 @@ export function validateLocaleStructure(localePath: string) { process.exit(1) } - const folderName = path.basename(localePath) - - if (`@lifeforge/${folderName}` !== packageJson.data.name) { + if (`@lifeforge/${lang}` !== packageJson.data.name) { logger.actionableError( - `The folder name "${folderName}" does not match the package name "${packageJson.data.name}"`, + `The folder name "${lang}" does not match the package name "${packageJson.data.name}"`, 'Please make sure the folder name matches the package name' ) @@ -61,18 +72,18 @@ export function validateLocaleStructure(localePath: string) { } const folderContents = fs - .readdirSync(localePath) + .readdirSync(targetDir) .filter(file => !['package.json', '.git'].includes(file)) if ( !folderContents.every( file => - fs.statSync(path.join(localePath, file)).isFile() && + fs.statSync(path.join(targetDir, file)).isFile() && file.endsWith('.json') ) ) { logger.actionableError( - `Locale "${folderName}" contains non-JSON files`, + `Locale "${lang}" contains non-JSON files`, 'Please make sure all files in the locale directory are JSON files' ) @@ -80,17 +91,19 @@ export function validateLocaleStructure(localePath: string) { } for (const file of folderContents) { - const filePath = path.join(localePath, file) + const filePath = path.join(targetDir, file) try { JSON.parse(fs.readFileSync(filePath, 'utf-8')) } catch { logger.actionableError( - `Locale "${folderName}" contains invalid JSON files "${file}"`, + `Locale "${lang}" contains invalid JSON files "${file}"`, 'Please make sure all files in the locale directory are valid JSON files' ) process.exit(1) } } + + logger.success(`Locale ${chalk.blue(lang)} is valid`) } diff --git a/tools/src/commands/locales/index.ts b/tools/src/commands/locales/index.ts index 063301c39..92816c411 100644 --- a/tools/src/commands/locales/index.ts +++ b/tools/src/commands/locales/index.ts @@ -5,6 +5,7 @@ import { listLocalesHandler } from './handlers/listLocalesHandler' import { publishLocaleHandler } from './handlers/publishLocaleHandler' import { uninstallLocaleHandler } from './handlers/uninstallLocaleHandler' import { upgradeLocaleHandler } from './handlers/upgradeLocalesHandler' +import { validateLocaleStructureHandler } from './handlers/validateLocaleStructure' export default function setup(program: Command): void { const command = program @@ -45,6 +46,12 @@ export default function setup(program: Command): void { ) .action(upgradeLocaleHandler) + command + .command('validate') + .description('Validate a language pack') + .argument('', 'Language code to validate') + .action(validateLocaleStructureHandler) + command .command('publish') .description('Publish a language pack to the registry')