feat: migrate locale files to a new @lifeforge/<owner>--lang- package structure and refactor locale validation

This commit is contained in:
melvinchia3636
2026-01-28 14:12:45 +08:00
parent 6f862ba1c8
commit 231d73064c
21 changed files with 147 additions and 106 deletions

2
.gitignore vendored
View File

@@ -57,4 +57,4 @@ keys
# user-generated files
apps
locales/*
!locales/en
!locales/lifeforge--lang-en

View File

@@ -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 <lang>` | Install a new language pack |
| `bun forge locales remove <lang>` | Remove an installed language pack |
| `bun forge locales validate <lang>` | Validate a language pack structure |
</section>
@@ -53,26 +54,26 @@ Installed language packs (4):
<section id="installing-a-language-pack">
## 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 <lang>
bun forge locales add <package-name>
```
For example, to install Japanese:
```bash
bun forge locales add ja
bun forge locales add lifeforge--lang-ja
```
<Alert className="mt-6" type="note">
Language packs are fetched from the official LifeForge repository at `lifeforge-app/lang-<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 `<owner>--lang-<code>`.
</Alert>
### What Happens During Installation
1. **Git Submodule** — The language pack is cloned as a git submodule into `locales/<lang>`
2. **Structure Validation** — The CLI verifies that `manifest.json` exists
1. **Package Installation** — The language pack is downloaded into `locales/<package-name>`
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
<Alert className="mt-6" type="important">
@@ -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 <lang>
bun forge locales remove <package-name>
```
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
<Alert className="mt-6" type="caution">
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) | <GithubUser username="melvinchia3636" /> |
| `ms` | Bahasa Malaysia | [lifeforge-app/lang-ms](https://github.com/lifeforge-app/lang-ms) | <GithubUser username="melvinchia3636" /> |
| `zh-CN` | 简体中文 | [lifeforge-app/lang-zh-CN](https://github.com/lifeforge-app/lang-zh-CN) | <GithubUser username="melvinchia3636" /> |
| `zh-TW` | 繁體中文 | [lifeforge-app/lang-zh-TW](https://github.com/lifeforge-app/lang-zh-TW) | <GithubUser username="melvinchia3636" /> |
| `tr` | Türkçe | [lifeforge-app/lang-tr](https://github.com/lifeforge-app/lang-tr) | <GithubUser username="tahabugracck" /> |
| Package Name | Language | Repository | Author / Contributors |
|--------------|----------|------------|--------|
| `lifeforge--lang-en` | English | [lifeforge-app/lang-en](https://github.com/lifeforge-app/lang-en) | <GithubUser username="melvinchia3636" /> |
| `melvinchia3636--lang-ms` | Bahasa Malaysia | [lifeforge-app/lang-ms](https://github.com/lifeforge-app/lang-ms) | <GithubUser username="melvinchia3636" /> |
| `melvinchia3636--lang-zh-CN` | 简体中文 | [lifeforge-app/lang-zh-CN](https://github.com/lifeforge-app/lang-zh-CN) | <GithubUser username="melvinchia3636" /> |
| `melvinchia3636--lang-zh-TW` | 繁體中文 | [lifeforge-app/lang-zh-TW](https://github.com/lifeforge-app/lang-zh-TW) | <GithubUser username="melvinchia3636" /> |
| `lifeforge--lang-tr` | Türkçe | [lifeforge-app/lang-tr](https://github.com/lifeforge-app/lang-tr) | <GithubUser username="tahabugracck" /> |
<Alert className="mt-6" type="tip">
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 <lang>
bun forge locales add <lang>
bun forge locales remove <package-name>
bun forge locales add <package-name>
```
### Git Submodule Errors

View File

@@ -26,29 +26,20 @@ LifeForge uses **[i18next](https://www.i18next.com/)** with **react-i18next** fo
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:language" className="translate-y-1" />
**4 Supported Languages** (for now)
**Community Driven Language Support**
</div>
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.
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:download" className="translate-y-1" />
**Dynamic Loading**
**Package-based Architecture**
</div>
Translations are fetched on-demand via HTTP backend, reducing initial bundle size
Translations are managed as NPM packages, allowing for easy updates and versioning.
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:folders" className="translate-y-1" />
**Namespace Architecture**
</div>
Modular organization with `apps.\*` and `common.*` namespaces for clean separation
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:tool" className="translate-y-1" />
**UI-based Management**
</div>
Dedicated Localization Manager tool for easy editing and maintenance
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:sparkles" className="translate-y-1" />
**AI Translation Assistance**
</div>
Built-in Gemini API integration for translation suggestions in the Localization Manager tool
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:alert-triangle" className="translate-y-1" />
**Missing Key Reporting**
@@ -62,13 +53,13 @@ LifeForge uses **[i18next](https://www.i18next.com/)** with **react-i18next** fo
### Module Locales (<code className='text-xl!'>apps.*</code> 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 (<code className='text-xl!'>common.*</code> 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
├── ...
```
</section>
@@ -266,35 +252,7 @@ When using `ForgeCLI` to create a module, locale files are automatically generat
</section>
<section id="localization-manager">
## Localization Manager
LifeForge includes a dedicated web-based **Localization Manager** tool for managing translations across your entire application:
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:binary-tree" className="translate-y-1" />
**Visual Tree Editor**
</div>
Navigate and edit locale keys in a hierarchical tree structure
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:columns" className="translate-y-1" />
**Side-by-Side Editing**
</div>
Edit translations for all languages simultaneously
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:sparkles" className="translate-y-1" />
**AI Translation Suggestions**
</div>
Get translation suggestions powered by Gemini API
- <div className="text-bg-800 dark:text-bg-100 flex items-center gap-2">
<Icon icon="tabler:search" className="translate-y-1" />
**Search Functionality**
</div>
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.
</section>
<section id="best-practices">
## Best Practices
@@ -373,27 +331,90 @@ When adding a key to English, immediately add placeholder or translated values t
<section id="creating-language-packs">
## 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-<code>` (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/<owner>--lang-<code>`:
* `<owner>`: Your GitHub username or organization name (e.g., `melvinchia3636`)
* `<code>`: 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.
#### <code className='text-lg!'>package.json</code> 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 <melvinchia623600@gmail.com>",
"lifeforge": {
"code": "ms",
"displayName": "Bahasa Malaysia",
"icon": "circle-flags:my",
"alternative": ["Bahasa Melayu", "Malay"]
}
}
```
<Alert className="mt-6" type="note">
The `icon` field uses [Iconify](https://icon-sets.iconify.design/) icon names. The `circle-flags` set is recommended for country flags.
</Alert>
**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 <lang>
```
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 <lang-code>
```
* **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`.
</section>

View File

@@ -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<void> {
const { fullName, targetDir } = normalizePackage(langCode, 'locale')
@@ -25,7 +25,7 @@ export async function publishLocaleHandler(langCode: string): Promise<void> {
}
logger.info('Validating locale structure...')
validateLocaleStructure(targetDir)
validateLocaleStructureHandler({ lang: langCode })
logger.debug('Validating module author...')
await validateLocalesAuthor(targetDir)

View File

@@ -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`)
}

View File

@@ -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('<lang>', 'Language code to validate')
.action(validateLocaleStructureHandler)
command
.command('publish')
.description('Publish a language pack to the registry')