{"_id":"@0x30-ch/localized-field","name":"@0x30-ch/localized-field","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@0x30-ch/localized-field","description":"Strapi custom field for per-locale translations without full i18n localization","version":"1.0.0","keywords":["strapi","plugin","localization","i18n","field","translation","locale"],"type":"commonjs","exports":{"./package.json":"./package.json","./strapi-admin":{"types":"./dist/admin/src/index.d.ts","source":"./admin/src/index.ts","import":"./dist/admin/index.mjs","require":"./dist/admin/index.js","default":"./dist/admin/index.js"},"./strapi-server":{"types":"./dist/server/src/index.d.ts","source":"./server/src/index.ts","import":"./dist/server/index.mjs","require":"./dist/server/index.js","default":"./dist/server/index.js"}},"scripts":{"build":"strapi-plugin build && npm run build:types","build:types":"tsc -p admin/tsconfig.json --declaration --emitDeclarationOnly --noEmit false --rootDir admin --outDir dist/admin && tsc -p server/tsconfig.json --declaration --emitDeclarationOnly --rootDir server --outDir dist/server","watch":"strapi-plugin watch","dev":"strapi-plugin watch","watch:link":"strapi-plugin watch:link","verify":"strapi-plugin verify","test:ts:front":"tsc -p admin/tsconfig.json","test:ts:back":"tsc -p server/tsconfig.json"},"dependencies":{"react-intl":"^7.1.11"},"devDependencies":{"@strapi/strapi":"^5.31.3","@strapi/sdk-plugin":"^5.3.2","@strapi/typescript-utils":"^5.31.3","prettier":"^3.6.2","typescript":"^5.8.3"},"peerDependencies":{"@strapi/strapi":"^5.18.1","@strapi/sdk-plugin":"^5.3.2","@strapi/design-system":"^2.0.0","@strapi/icons":"^2.0.0","react":"^18.3.1","react-dom":"^18.3.1","react-router-dom":"^6.30.1","styled-components":"^6.1.19"},"strapi":{"kind":"plugin","name":"localized-field","displayName":"Localized Field","description":"Custom field storing translations per locale as JSON"},"license":"MIT","repository":{"type":"git","url":"git+ssh://git@github.com/0x30-ch/strapi-plugins.git"},"bugs":{"url":"https://github.com/0x30-ch/strapi-plugins/issues"},"homepage":"https://github.com/0x30-ch/strapi-plugins#readme","author":{"name":"jeanvier","email":"hello@0x30.ch"},"_id":"@0x30-ch/localized-field@1.0.0","gitHead":"4cbe5e6adecc7feecdabfe3d64ec911f77b5a7ba","_nodeVersion":"22.18.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-+6sLpHhuSkesLLO2vK/VytwxDNd/Xmtov0cUHd5dGUtN891pRM3bsGJNxhRR4/7iha8wkNLsRjOAsAIAG2OoEA==","shasum":"684bdd22533d1a1d61482957d6831774ceb4bf71","tarball":"https://registry.npmjs.org/@0x30-ch/localized-field/-/localized-field-1.0.0.tgz","fileCount":17,"unpackedSize":1706187,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIA8gFd4TsxMHpRynVXJAE4p+eRYq2RlyY737Jn4rMIjeAiEA9fALdDFMvo3XawsZW50b6mAqVw0/3fhO7IYZyZqVFrk="}]},"_npmUser":{"name":"monsieurjanvier","email":"damien.grossfeld@gmail.com"},"directories":{},"maintainers":[{"name":"monsieurjanvier","email":"damien.grossfeld@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/localized-field_1.0.0_1770676347920_0.3682073323298589"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-09T22:32:27.848Z","1.0.0":"2026-02-09T22:32:28.161Z","modified":"2026-02-09T22:32:28.390Z"},"maintainers":[{"name":"monsieurjanvier","email":"damien.grossfeld@gmail.com"}],"description":"Strapi custom field for per-locale translations without full i18n localization","homepage":"https://github.com/0x30-ch/strapi-plugins#readme","keywords":["strapi","plugin","localization","i18n","field","translation","locale"],"repository":{"type":"git","url":"git+ssh://git@github.com/0x30-ch/strapi-plugins.git"},"author":{"name":"jeanvier","email":"hello@0x30.ch"},"bugs":{"url":"https://github.com/0x30-ch/strapi-plugins/issues"},"license":"MIT","readme":"# Strapi Localized Field Plugin\n\nA 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.\n\n![Strapi](https://img.shields.io/badge/Strapi-5.x-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue) ![Version](https://img.shields.io/badge/version-1.0.0-brightgreen)\n\n## Features\n\n- **Per-Locale Translations**: Store translations for all locales in a single JSON field on a single entry\n- **No Full i18n Required**: Works alongside Strapi's i18n plugin without duplicating entire entries per locale\n- **Compact Locale Tabs**: Inline tab UI with status dots showing filled/empty state and a fill counter badge\n- **Short & Long Text**: Choose between single-line input or textarea per field\n- **Configurable Fallback**: Three strategies when a locale is missing — default locale, first available, or none\n- **Automatic API Resolution**: Middleware resolves translations in content API responses based on `?locale=` query param\n- **Populate All Locales**: Request all translations via `?plugins[localized-field][populateLocalization]=true`\n- **Component & Dynamic Zone Support**: Works inside components, repeatable components, and dynamic zones\n- **Full i18n**: All admin UI strings use `react-intl` for internationalization\n\n## Installation\n\n```bash\nnpm install @0x30-ch/localized-field\n# or\nyarn add @0x30-ch/localized-field\n# or\npnpm add @0x30-ch/localized-field\n```\n\n## Configuration\n\n### 1. Enable the Plugin\n\n```typescript\n// config/plugins.ts\nexport default () => ({\n  'localized-field': {\n    enabled: true,\n    resolve: './node_modules/@0x30-ch/localized-field',\n  },\n});\n```\n\n### 2. Add the Custom Field\n\nIn the Strapi Content-Type Builder, add the **Localized string** custom field to your content type. Configure:\n\n| Option | Values | Default | Description |\n|--------|--------|---------|-------------|\n| **Field type** | `Short text`, `Long text` | `Short text` | Single-line input or textarea |\n| **Default locale** | Any locale code | *(auto-detected)* | Override which locale tab opens first and is used for fallback |\n| **Fallback strategy** | `Default locale`, `First available`, `None` | `Default locale` | How to resolve when the requested locale has no translation |\n\n### 3. Restart Your Server\n\nAfter configuration, restart your Strapi server to apply the schema changes.\n\n## Usage\n\n### Admin Interface\n\n1. **Open** any entry with a localized field\n2. **Switch locale** by clicking the compact tab chips — each shows a status dot (filled = has content)\n3. **Type** your translation in the input field\n4. **Track progress** with the badge showing filled count (e.g., `2/4`)\n5. **Save** the entry — all translations are stored as a single JSON value\n\n### Content API\n\nThe plugin registers a global Koa middleware that automatically resolves localized fields in content API responses.\n\n#### Resolve a Single Locale\n\nPass `?locale=` to get the resolved string value instead of the raw JSON object:\n\n```bash\n# Returns resolved string for French\nGET /api/tags?locale=fr\n```\n\n**Response:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"label\": \"Bonjour\"\n    }\n  ]\n}\n```\n\n#### Get All Translations\n\nAdd the `populateLocalization` plugin param to receive all locale values:\n\n```bash\nGET /api/tags?locale=fr&plugins[localized-field][populateLocalization]=true\n```\n\n**Response:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"label\": \"Bonjour\",\n      \"label_localizations\": {\n        \"en\": \"Hello\",\n        \"fr\": \"Bonjour\",\n        \"de\": \"Hallo\"\n      }\n    }\n  ]\n}\n```\n\n#### Without Locale Param\n\nWhen no `?locale=` is provided, the raw JSON object is returned as-is:\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"label\": {\n        \"en\": \"Hello\",\n        \"fr\": \"Bonjour\",\n        \"de\": \"Hallo\"\n      }\n    }\n  ]\n}\n```\n\n### Fallback Strategies\n\n| Strategy | Behavior |\n|----------|----------|\n| **Default locale** | Falls back to the configured default locale (or Strapi's i18n default) |\n| **First available** | Returns the first non-empty translation found |\n| **None** | Returns `null` if the requested locale has no translation |\n\n## Data Structure\n\nThe field stores a JSON object mapping locale codes to translation strings:\n\n```json\n{\n  \"en\": \"Hello world\",\n  \"fr\": \"Bonjour le monde\",\n  \"de\": \"Hallo Welt\"\n}\n```\n\nThis is stored in a single `json` column in the database — no additional tables or relations needed.\n\n## Development\n\n### Prerequisites\n\n- Node.js >= 18.0.0\n- Strapi 5.x\n- i18n plugin enabled (for locale detection)\n\n### Setup\n\n```bash\ngit clone https://github.com/0x30-ch/strapi-plugins.git\ncd strapi-plugins\npnpm install\n\n# Start development\npnpm dev\n\n# Build plugin\ncd packages/localized-field\nnpm run build\n\n# Type checking\nnpm run test:ts:front  # Frontend types\nnpm run test:ts:back   # Backend types\n\n# Verify plugin structure\nnpm run verify\n```\n\n### Project Structure\n\n```\npackages/localized-field/\n├── admin/                        # Frontend (React + Strapi Design System v2)\n│   ├── src/\n│   │   ├── components/\n│   │   │   ├── LocalizedInput.tsx       # Main input with locale tabs\n│   │   │   └── LocalizedFieldIcon.tsx   # Field icon for content-type builder\n│   │   ├── utils/\n│   │   │   └── getTranslation.ts        # i18n helper\n│   │   ├── translations/\n│   │   │   └── en.json                  # English translations\n│   │   ├── pluginId.ts                  # Plugin identifier\n│   │   └── index.ts                     # Plugin registration + custom field config\n│   └── tsconfig.json\n├── server/                       # Backend logic\n│   ├── src/\n│   │   ├── register.ts                  # Custom field server registration\n│   │   ├── bootstrap.ts                 # Global middleware for API resolution\n│   │   ├── utils/\n│   │   │   └── localized.ts             # Core logic (collect, resolve, fallback)\n│   │   ├── config/                      # Plugin configuration\n│   │   ├── controllers/                 # (empty — no admin routes needed)\n│   │   ├── services/                    # (empty — logic in middleware)\n│   │   └── routes/                      # (empty — no custom routes)\n│   └── tsconfig.json\n└── package.json\n```\n\n## Architecture\n\n### How It Works\n\n1. **Registration**: The plugin registers a `localizedString` custom field of type `json`\n2. **Admin UI**: The `LocalizedInput` component fetches available locales from `/i18n/locales` and renders compact tab chips with an input per locale\n3. **Storage**: All translations are stored as a single JSON object in one database column\n4. **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\n5. **Deep Resolution**: The middleware recursively walks components, repeatable components, and dynamic zones to resolve nested localized fields\n\n### Why Not Full i18n?\n\nStrapi's built-in i18n duplicates the **entire entry** per locale. This plugin is for cases where:\n\n- You need translations for specific fields only (SEO, labels, slugs)\n- Your content structure is locale-independent (same images, same relations)\n- You want to manage all translations in one place, on one entry\n- You need a lightweight alternative without entry duplication\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n\n## Acknowledgments\n\n- [Strapi](https://strapi.io/) - Headless CMS framework\n- [0x30](https://github.com/0x30-ch) - Plugin author\n\n## Support\n\n- [Issues](https://github.com/0x30-ch/strapi-plugins/issues)\n- [Documentation](https://github.com/0x30-ch/strapi-plugins/tree/main/packages/localized-field)\n","readmeFilename":"README.md","_rev":"1-1f0980c9699c15c57de032388668b90e"}