mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
feat: migrate locale files to a new @lifeforge/<owner>--lang- package structure and refactor locale validation
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -57,4 +57,4 @@ keys
|
||||
# user-generated files
|
||||
apps
|
||||
locales/*
|
||||
!locales/en
|
||||
!locales/lifeforge--lang-en
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user