@0x30-ch/localized-field
v1.0.0
Published
Strapi custom field for per-locale translations without full i18n localization
Maintainers
Readme
Strapi Localized Field Plugin
A Strapi 5 custom field that stores per-locale translations as JSON — without requiring full i18n content localization. Perfect for fields like SEO titles, button labels, or any text that needs translations while the entry itself stays in a single locale.
Features
- Per-Locale Translations: Store translations for all locales in a single JSON field on a single entry
- No Full i18n Required: Works alongside Strapi's i18n plugin without duplicating entire entries per locale
- Compact Locale Tabs: Inline tab UI with status dots showing filled/empty state and a fill counter badge
- Short & Long Text: Choose between single-line input or textarea per field
- Configurable Fallback: Three strategies when a locale is missing — default locale, first available, or none
- Automatic API Resolution: Middleware resolves translations in content API responses based on
?locale=query param - Populate All Locales: Request all translations via
?plugins[localized-field][populateLocalization]=true - Component & Dynamic Zone Support: Works inside components, repeatable components, and dynamic zones
- Full i18n: All admin UI strings use
react-intlfor internationalization
Installation
npm install @0x30-ch/localized-field
# or
yarn add @0x30-ch/localized-field
# or
pnpm add @0x30-ch/localized-fieldConfiguration
1. Enable the Plugin
// config/plugins.ts
export default () => ({
'localized-field': {
enabled: true,
resolve: './node_modules/@0x30-ch/localized-field',
},
});2. Add the Custom Field
In the Strapi Content-Type Builder, add the Localized string custom field to your content type. Configure:
| Option | Values | Default | Description |
|--------|--------|---------|-------------|
| Field type | Short text, Long text | Short text | Single-line input or textarea |
| Default locale | Any locale code | (auto-detected) | Override which locale tab opens first and is used for fallback |
| Fallback strategy | Default locale, First available, None | Default locale | How to resolve when the requested locale has no translation |
3. Restart Your Server
After configuration, restart your Strapi server to apply the schema changes.
Usage
Admin Interface
- Open any entry with a localized field
- Switch locale by clicking the compact tab chips — each shows a status dot (filled = has content)
- Type your translation in the input field
- Track progress with the badge showing filled count (e.g.,
2/4) - Save the entry — all translations are stored as a single JSON value
Content API
The plugin registers a global Koa middleware that automatically resolves localized fields in content API responses.
Resolve a Single Locale
Pass ?locale= to get the resolved string value instead of the raw JSON object:
# Returns resolved string for French
GET /api/tags?locale=frResponse:
{
"data": [
{
"id": 1,
"label": "Bonjour"
}
]
}Get All Translations
Add the populateLocalization plugin param to receive all locale values:
GET /api/tags?locale=fr&plugins[localized-field][populateLocalization]=trueResponse:
{
"data": [
{
"id": 1,
"label": "Bonjour",
"label_localizations": {
"en": "Hello",
"fr": "Bonjour",
"de": "Hallo"
}
}
]
}Without Locale Param
When no ?locale= is provided, the raw JSON object is returned as-is:
{
"data": [
{
"id": 1,
"label": {
"en": "Hello",
"fr": "Bonjour",
"de": "Hallo"
}
}
]
}Fallback Strategies
| Strategy | Behavior |
|----------|----------|
| Default locale | Falls back to the configured default locale (or Strapi's i18n default) |
| First available | Returns the first non-empty translation found |
| None | Returns null if the requested locale has no translation |
Data Structure
The field stores a JSON object mapping locale codes to translation strings:
{
"en": "Hello world",
"fr": "Bonjour le monde",
"de": "Hallo Welt"
}This is stored in a single json column in the database — no additional tables or relations needed.
Development
Prerequisites
- Node.js >= 18.0.0
- Strapi 5.x
- i18n plugin enabled (for locale detection)
Setup
git clone https://github.com/0x30-ch/strapi-plugins.git
cd strapi-plugins
pnpm install
# Start development
pnpm dev
# Build plugin
cd packages/localized-field
npm run build
# Type checking
npm run test:ts:front # Frontend types
npm run test:ts:back # Backend types
# Verify plugin structure
npm run verifyProject Structure
packages/localized-field/
├── admin/ # Frontend (React + Strapi Design System v2)
│ ├── src/
│ │ ├── components/
│ │ │ ├── LocalizedInput.tsx # Main input with locale tabs
│ │ │ └── LocalizedFieldIcon.tsx # Field icon for content-type builder
│ │ ├── utils/
│ │ │ └── getTranslation.ts # i18n helper
│ │ ├── translations/
│ │ │ └── en.json # English translations
│ │ ├── pluginId.ts # Plugin identifier
│ │ └── index.ts # Plugin registration + custom field config
│ └── tsconfig.json
├── server/ # Backend logic
│ ├── src/
│ │ ├── register.ts # Custom field server registration
│ │ ├── bootstrap.ts # Global middleware for API resolution
│ │ ├── utils/
│ │ │ └── localized.ts # Core logic (collect, resolve, fallback)
│ │ ├── config/ # Plugin configuration
│ │ ├── controllers/ # (empty — no admin routes needed)
│ │ ├── services/ # (empty — logic in middleware)
│ │ └── routes/ # (empty — no custom routes)
│ └── tsconfig.json
└── package.jsonArchitecture
How It Works
- Registration: The plugin registers a
localizedStringcustom field of typejson - Admin UI: The
LocalizedInputcomponent fetches available locales from/i18n/localesand renders compact tab chips with an input per locale - Storage: All translations are stored as a single JSON object in one database column
- API Resolution: A global Koa middleware intercepts content API responses, detects localized fields in the schema, and resolves them based on the
?locale=query parameter - Deep Resolution: The middleware recursively walks components, repeatable components, and dynamic zones to resolve nested localized fields
Why Not Full i18n?
Strapi's built-in i18n duplicates the entire entry per locale. This plugin is for cases where:
- You need translations for specific fields only (SEO, labels, slugs)
- Your content structure is locale-independent (same images, same relations)
- You want to manage all translations in one place, on one entry
- You need a lightweight alternative without entry duplication
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT License - see LICENSE file for details.
