--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 William Iommi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Slider/Slider.Wrapper.tsx:
--------------------------------------------------------------------------------
1 | import {Dispatch, SetStateAction} from 'react'
2 | import {I18nFieldsConfigUI} from '../../types/I18nFields'
3 | import {InternalLocale} from '../../types/Locale'
4 | import Slide from './Slider.Slide'
5 | import useSlider from '../../hooks/useSlider'
6 |
7 | interface SliderWrapperProps {
8 | name: string | undefined
9 | pluginUi: I18nFieldsConfigUI
10 | locales: InternalLocale[]
11 | activeLocale: InternalLocale
12 | onClick: Dispatch>
13 | }
14 | const SliderWrapper = ({name, pluginUi, locales, activeLocale, onClick}: SliderWrapperProps) => {
15 | const keenRef = useSlider()
16 | return (
17 |
100 |
101 | )
102 | }
103 |
104 | export default I18nDefaultField
105 |
--------------------------------------------------------------------------------
/src/style/style.scss:
--------------------------------------------------------------------------------
1 | [data-read-only='true'] {
2 | .-label {
3 | filter: grayscale(1);
4 | color: gray;
5 | }
6 | }
7 |
8 | .i18n--field-wrapper-top {
9 | position: relative;
10 | padding-right: 0;
11 | &[data-ui='dropdown'] {
12 | padding-bottom: 10px;
13 | padding-right: 100px;
14 | }
15 | }
16 |
17 | .i18n--field-member {
18 | &-container {
19 | position: relative;
20 | }
21 |
22 | &.--hidden {
23 | position: absolute;
24 | width: 100%;
25 | top: 0;
26 | opacity: 0;
27 | user-select: none;
28 | z-index: -1;
29 | textarea {
30 | height: 1px;
31 | }
32 | }
33 | }
34 |
35 | .i18n--slider-language {
36 | position: relative;
37 | display: flex;
38 | margin: 5px 0 10px 0;
39 | transition: opacity 0.3s ease-in-out;
40 | order: 0;
41 |
42 | &::before,
43 | &::after {
44 | position: absolute;
45 | width: 30px;
46 | height: 100%;
47 | z-index: 2;
48 | }
49 |
50 | &[data-begin='false'] {
51 | &::before {
52 | content: '';
53 | left: 0;
54 | background-image: linear-gradient(to right, var(--i18n-base-bg), transparent);
55 | }
56 | }
57 |
58 | &[data-end='false'] {
59 | &::after {
60 | content: '';
61 | right: 0;
62 | background-image: linear-gradient(to left, var(--i18n-base-bg), transparent);
63 | }
64 | }
65 |
66 | &[data-position='bottom'] {
67 | order: 1;
68 | margin: 7px 0 0;
69 | }
70 |
71 | &-slide {
72 | display: flex;
73 | align-items: center;
74 | justify-content: center;
75 | flex: 0 0 auto;
76 | width: auto !important;
77 | padding: 0 5px;
78 | cursor: pointer;
79 | box-sizing: border-box;
80 | font-size: 0.875rem;
81 |
82 | &:not([data-ui='border']) {
83 | background-color: transparent;
84 | &[data-selected='true'] {
85 | background-color: var(--i18n-bg-selected);
86 | }
87 | &:hover {
88 | background-color: var(--i18n-bg-hover);
89 | &[data-selected='true'] {
90 | background-color: var(--i18n-bg-selected);
91 | }
92 | }
93 | }
94 |
95 | &[data-ui='border'] {
96 | border-bottom: 2px solid transparent;
97 | border-bottom-color: transparent;
98 | &[data-selected='true'] {
99 | border-bottom-color: var(--i18n-bg-selected);
100 | }
101 | &:hover {
102 | border-bottom-color: var(--i18n-bg-hover);
103 | &[data-selected='true'] {
104 | border-bottom-color: var(--i18n-bg-selected);
105 | }
106 | }
107 | }
108 | }
109 | }
110 |
111 | .i18n--dropdown-button {
112 | background: none !important;
113 | text-align: center;
114 | display: flex;
115 | align-items: center;
116 | justify-content: center;
117 | position: absolute !important;
118 | top: 0 !important;
119 | right: 0 !important;
120 | max-width: 100px;
121 |
122 | span {
123 | padding: 0 !important;
124 | }
125 |
126 | .-content {
127 | display: flex;
128 | align-items: center;
129 | justify-content: center;
130 | color: initial !important;
131 | overflow: hidden;
132 | padding: 0 2px;
133 |
134 | .-label {
135 | font-size: 0.875rem;
136 | margin-right: 20px;
137 | white-space: nowrap;
138 | overflow: hidden;
139 | text-overflow: ellipsis;
140 | padding: 3px 0 3px 5px !important;
141 | }
142 |
143 | .-icon {
144 | position: absolute;
145 | right: 0px;
146 | color: initial !important;
147 | }
148 | }
149 | }
150 |
151 | .i18n--dropdown-menu {
152 | max-width: 300px;
153 | max-height: 200px;
154 | text-align: center;
155 |
156 | button {
157 | background: none !important;
158 | }
159 | }
160 |
161 | .i18n--dropdown-menu-item {
162 | display: flex;
163 | align-items: center;
164 | justify-content: flex-end;
165 | cursor: pointer;
166 | font-size: 0.875rem;
167 | padding: 4px 7px;
168 | color: initial;
169 | &:hover {
170 | background-color: var(--i18n-bg-hover);
171 | color: initial;
172 | border-radius: 3px;
173 | }
174 | }
175 |
176 | .i18n--select-language {
177 | max-width: 100px;
178 | position: absolute;
179 | top: 50%;
180 | right: 0;
181 | transform: translateY(-50%);
182 | }
183 |
--------------------------------------------------------------------------------
/src/hooks/useLocalesInfo.ts:
--------------------------------------------------------------------------------
1 | import {ConditionalProperty, FieldMember, ObjectMember, SanityDocument, useFormValue} from 'sanity'
2 | import {I18nStringLocale, InternalLocale, Locale} from '../types/Locale'
3 | import {Dispatch, SetStateAction, useMemo, useState} from 'react'
4 | import useUserInfo from './useUserInfo'
5 | import mergeLocaleConfiguration from '../lib/mergeLocaleConfiguration'
6 | import validateLocaleRestrictions from '../lib/validateLocaleRestrictions'
7 | import checkFieldError from '../lib/checkFieldError'
8 | import checkFieldChanged from '../lib/checkFieldChanged'
9 |
10 | interface useLocalesInfoProps {
11 | locales: Locale[] // locales coming from plugin configuration
12 | members: ObjectMember[] // members of the i18n object
13 | hasGlobalError: boolean // globar error at object level
14 | fieldLocales: I18nStringLocale[] | undefined // specific nested field locales override
15 | fieldHidden: ConditionalProperty | undefined // specific nested field hidden ConditionalProperty
16 | fieldReadOnly: ConditionalProperty | undefined // specific nested field readOnly ConditionalProperty
17 | currentPath: string //the 'name/path' of the object
18 | }
19 |
20 | interface useLocalesInfoResponse {
21 | availableLocales: InternalLocale[]
22 | activeLocale: InternalLocale | undefined
23 | setCurrentLocaleCode: Dispatch>
24 | }
25 |
26 | const useLocalesInfo = ({
27 | locales,
28 | members,
29 | hasGlobalError,
30 | fieldLocales,
31 | fieldHidden,
32 | fieldReadOnly,
33 | currentPath,
34 | }: useLocalesInfoProps): useLocalesInfoResponse => {
35 | const document = useFormValue([]) as SanityDocument // get the current SanityDocument
36 | const fieldValue = useFormValue([currentPath]) as {[key: string]: unknown} // current field value (an object of locales)
37 | const {currentUser, userRoles} = useUserInfo() // get info about the current user
38 |
39 | // filter locales by configuration and sort, default first
40 | const availableLocales = useMemo(
41 | () =>
42 | locales
43 | // merge global config w/ field level config
44 | .map((locale) => mergeLocaleConfiguration(locale, fieldLocales, fieldHidden, fieldReadOnly))
45 | // validate restrictions by visibleFor, editableFor, readOnly and hidden attributes
46 | .map((locale) =>
47 | validateLocaleRestrictions({
48 | userRoles,
49 | locale,
50 | currentUser,
51 | fieldValue,
52 | document,
53 | members,
54 | })
55 | )
56 | // remove all possible hidden locales after restrictions
57 | .filter((locale) => !locale.isHidden)
58 | // check for field errors level, if global error present no need to show specific error
59 | .map((locale) => {
60 | if (hasGlobalError) return locale
61 | return checkFieldError(locale, members)
62 | })
63 | // check for field changed
64 | .map((locale) => checkFieldChanged(locale, members))
65 | // put at first position the default locale or the [0] if no default is present
66 | .sort((a, b) => Number(!!b.default) - Number(!!a.default)),
67 | [
68 | locales,
69 | fieldLocales,
70 | fieldHidden,
71 | fieldReadOnly,
72 | userRoles,
73 | currentUser,
74 | fieldValue,
75 | document,
76 | hasGlobalError,
77 | members,
78 | ]
79 | )
80 |
81 | const [currentLocaleCode, setCurrentLocaleCode] = useState(availableLocales[0].code)
82 |
83 | const focusedMember = useMemo(
84 | () => (members as FieldMember[]).find((member) => member.field.focused),
85 | [members]
86 | )
87 |
88 | const activeLocale = useMemo(() => {
89 | let locale = availableLocales.find((curr) => curr.code === focusedMember?.name)
90 | if (!locale) {
91 | locale = availableLocales.find((curr) => curr.code === currentLocaleCode)
92 | }
93 | if (!locale) locale = availableLocales[0]
94 | if (locale.code !== currentLocaleCode) {
95 | setCurrentLocaleCode(locale.code)
96 | }
97 | return locale
98 | }, [availableLocales, focusedMember, currentLocaleCode])
99 |
100 | return {availableLocales, activeLocale, setCurrentLocaleCode}
101 | }
102 |
103 | export default useLocalesInfo
104 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: CI & Release
3 |
4 | # Workflow name based on selected inputs. Fallback to default Github naming when expression evaluates to empty string
5 | run-name: >-
6 | ${{
7 | inputs.release && inputs.test && format('Build {0} ➤ Test ➤ Publish to NPM', github.ref_name) ||
8 | inputs.release && !inputs.test && format('Build {0} ➤ Skip Tests ➤ Publish to NPM', github.ref_name) ||
9 | github.event_name == 'workflow_dispatch' && inputs.test && format('Build {0} ➤ Test', github.ref_name) ||
10 | github.event_name == 'workflow_dispatch' && !inputs.test && format('Build {0} ➤ Skip Tests', github.ref_name) ||
11 | ''
12 | }}
13 |
14 | on:
15 | # Build on pushes branches that have a PR (including drafts)
16 | pull_request:
17 | # Build on commits pushed to branches without a PR if it's in the allowlist
18 | push:
19 | branches: [main]
20 | # https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow
21 | workflow_dispatch:
22 | inputs:
23 | test:
24 | description: Run tests
25 | required: true
26 | default: true
27 | type: boolean
28 | release:
29 | description: Release new version
30 | required: true
31 | default: false
32 | type: boolean
33 |
34 | concurrency:
35 | # On PRs builds will cancel if new pushes happen before the CI completes, as it defines `github.head_ref` and gives it the name of the branch the PR wants to merge into
36 | # Otherwise `github.run_id` ensures that you can quickly merge a queue of PRs without causing tests to auto cancel on any of the commits pushed to main.
37 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
38 | cancel-in-progress: true
39 |
40 | jobs:
41 | build:
42 | runs-on: ubuntu-latest
43 | name: Lint & Build
44 | steps:
45 | - uses: actions/checkout@v3
46 | - uses: actions/setup-node@v3
47 | with:
48 | cache: npm
49 | node-version: lts/*
50 | - run: npm ci
51 | # Linting can be skipped
52 | - run: npm run lint --if-present
53 | if: github.event.inputs.test != 'false'
54 | # But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early
55 | - run: npm run prepublishOnly --if-present
56 |
57 | test:
58 | needs: build
59 | # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main
60 | if: github.event.inputs.test != 'false'
61 | runs-on: ${{ matrix.os }}
62 | name: Node.js ${{ matrix.node }} / ${{ matrix.os }}
63 | strategy:
64 | # A test failing on windows doesn't mean it'll fail on macos. It's useful to let all tests run to its completion to get the full picture
65 | fail-fast: false
66 | matrix:
67 | # Run the testing suite on each major OS with the latest LTS release of Node.js
68 | os: [macos-latest, ubuntu-latest, windows-latest]
69 | node: [lts/*]
70 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner
71 | include:
72 | - os: ubuntu-latest
73 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life
74 | node: lts/-2
75 | - os: ubuntu-latest
76 | # Test the actively developed version that will become the latest LTS release next October
77 | node: current
78 | steps:
79 | # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF
80 | - name: Set git to use LF
81 | if: matrix.os == 'windows-latest'
82 | run: |
83 | git config --global core.autocrlf false
84 | git config --global core.eol lf
85 | - uses: actions/checkout@v3
86 | - uses: actions/setup-node@v3
87 | with:
88 | cache: npm
89 | node-version: ${{ matrix.node }}
90 | - run: npm i
91 | - run: npm test --if-present
92 |
93 | release:
94 | needs: [build, test]
95 | # only run if opt-in during workflow_dispatch
96 | if: always() && github.event.inputs.release == 'true' && needs.build.result != 'failure' && needs.test.result != 'failure' && needs.test.result != 'cancelled'
97 | runs-on: ubuntu-latest
98 | name: Semantic release
99 | steps:
100 | - uses: actions/checkout@v3
101 | with:
102 | # Need to fetch entire commit history to
103 | # analyze every commit since last release
104 | fetch-depth: 0
105 | - uses: actions/setup-node@v3
106 | with:
107 | cache: npm
108 | node-version: lts/*
109 | - run: npm ci
110 | # Branches that will release new versions are defined in .releaserc.json
111 | # @TODO remove --dry-run after verifying everything is good to go
112 | - run: npx semantic-release
113 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state
114 | # e.g. git tags were pushed but it exited before `npm publish`
115 | if: always()
116 | env:
117 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
119 | # Re-run semantic release with rich logs if it failed to publish for easier debugging
120 | - run: npx semantic-release --debug
121 | if: failure()
122 | env:
123 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
124 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
125 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # I18n fields
2 |
3 | An alternative way to manage localization at field level in your Sanity Studio.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | - [⚡️ Features](#%EF%B8%8F-features)
14 | - [🔌 Installation](#-installation)
15 | - [🧑💻 Usage](#-usage)
16 | - [⚙️ Plugin Configuration](#%EF%B8%8F-plugin-configuration)
17 | - [🔧 Field Configuration](#-field-configuration)
18 | - [🗃️ Data Model](#%EF%B8%8F-data-model)
19 | - [🚨 Validation](#-validation)
20 | - [🤩 Examples Examples Examples](#-examples-examples-examples)
21 | - [👀 Future features](#-future-features)
22 | - [📝 License](#-license)
23 | - [🧪 Develop & test](#-develop--test)
24 |
25 |
26 | ## ⚡️ Features
27 |
28 | - Sanity v3 plugin.
29 | - Field-level localization for the following Sanity types: `string`, `text`, and `number`.
30 | - Optional UI (slider or dropdown).
31 | - Locale visibility by user roles.
32 | - Locale read-only by user roles.
33 | - Object Validation.
34 | - Customization available also at field level.
35 | - Customizable types prefix.
36 |
37 |
38 | ## 🔌 Installation
39 |
40 | ```sh
41 | npm install sanity-plugin-i18n-fields
42 | ```
43 |
44 |
45 | ## 🧑💻 Usage
46 |
47 | Add it as a plugin in `sanity.config.ts` (or .js):
48 |
49 | ```ts
50 | import {defineConfig} from 'sanity'
51 | import {I18nFields} from 'sanity-plugin-i18n-fields'
52 |
53 | export default defineConfig({
54 | //...
55 | plugins: [I18nFields({
56 | // your configuration here
57 | })],
58 | })
59 | ```
60 | The plugin will provide three new types: `i18n.string`, `i18n.text`, and `i18n.number`. All three types will be objects with a dynamic number of fields based on the localizations provided during configuration.
61 |
62 |
63 | ## ⚙️ Plugin Configuration
64 | This is the main configuration of the plugin, and the available options are as follows:
65 | ```ts
66 | {
67 | prefix?: string // You can configure the prefix of the types created by the plugin. If you are already using them or prefer a different name for any reason, you can change it. The default is 'i18n'.
68 | // The 'ui' option allows you to customize the appearance of the plugin's UI. By default, it is set to 'slider'.
69 | ui?: {
70 | type?: 'slider' | 'dropdown' // The UI of the plugin, Default is 'slider'
71 | position?: 'top' | 'bottom' // You can specify the position of the 'slider' above or below the input field, with the default being 'bottom'.
72 | selected?: 'border' | 'background' // For the 'slider' type, you can configure the UI of the selected locale, and the default setting is 'border'.
73 | },
74 | // The 'locales' option is the core of the configuration, enabling you to set up all available locales for your project.
75 | locales: [
76 | {
77 | code: string // the code of the locale
78 | label: ReactNode | ComponentType // the label of the locale
79 | title: string // the title of the locale
80 | default?: boolean // This is the flag to identify the default locale. If set to true, the locale is placed in the first position.
81 | visibleFor?: string[] // You can define a list of roles for which this locale is visible. By using the '!' operator, you can make it not visible.
82 | editableFor?: string[] // You can define a list of roles for which this locale is editable. The '!' operator allows you to specify the opposite condition.
83 | },
84 | // other locales
85 | ]
86 | }
87 | ```
88 | Sample configuration:
89 | ```ts
90 | import {defineConfig} from 'sanity'
91 | import {I18nFields} from 'sanity-plugin-i18n-fields'
92 |
93 | export default defineConfig({
94 | //...
95 | plugins: [I18nFields({
96 | ui: {
97 | position: 'bottom'
98 | },
99 | locales: [
100 | {code: 'en', label: '🇬🇧', title: 'English', default: true},
101 | {code: 'en_us', label: '🇺🇸🇬🇧', title: 'American English'},
102 | {code: 'it', label: '🇮🇹', title: 'Italian', visibleFor: ['it_editor']}, // country visible only for administrator and it_editor roles
103 | {code: 'es', label: '🇪🇸', title: 'Spanish'},
104 | ]
105 | })],
106 | })
107 | ```
108 |
109 |
110 | ## 🔧 Field Configuration
111 | Other than global configuration, you can customize your configuration at field level. For example, for a specific field, you can have a dropdown layout or hide a particular locale.
112 | ```ts
113 | import {ConditionalProperty, NumberOptions, StringOptions} from 'sanity'
114 |
115 | export default defineType({
116 | type: 'document',
117 | name: 'myDocument',
118 | title: 'My Document',
119 | fields: [
120 | defineField({
121 | type: 'i18n.string' | 'i18n.text' | 'i18n.number',
122 | // ...
123 | options: {
124 | ui?: {
125 | type?: 'slider' | 'dropdown'
126 | position?: 'top' | 'bottom'
127 | selected?: 'border' | 'background'
128 | },
129 | locales?: [
130 | {
131 | code: string // the code of the locale. MUST be the same as the one used in the global configuration.
132 | readOnly?: ConditionalProperty
133 | hidden?: ConditionalProperty
134 | options?: StringOptions | { rows?:number } | NumberOptions
135 | visibleFor?: string[] // same as global configuration
136 | editableFor?: string[] // same as global configuration
137 | },
138 | // other locales
139 | ]
140 | }
141 | })
142 | ]
143 | })
144 | ```
145 |
146 |
147 | ## 🗃️ Data model
148 | ```ts
149 | // sample with 'en', 'en_us', 'it' and 'es' locales
150 |
151 | {
152 | _type: 'i18n.string',
153 | en: string,
154 | en_us: string,
155 | it: string,
156 | es: string,
157 | }
158 |
159 | {
160 | _type: 'i18n.text',
161 | en: string,
162 | en_us: string,
163 | it: string,
164 | es: string,
165 | }
166 |
167 | {
168 | _type: 'i18n.number',
169 | en: number,
170 | en_us: number,
171 | it: number,
172 | es: number,
173 | }
174 | ```
175 |
176 |
177 | ## 🚨 Validation
178 | Since the new types introduced by the plugin are objects, you can use [children validation](https://www.sanity.io/docs/validation#9e69d5db6f72) to apply specific validation to a particular locale.
179 | All error/warning messages are then collected and visible near the title of your field or in the right menu.
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 | ## 🤩 Examples Examples Examples
190 |
191 | - [Basic Configuration](docs/examples/basic-configuration.md)
192 | - [Global user roles visibility](docs/examples/global-user-roles-visibility.md)
193 | - [String field](docs/examples/string-field.md)
194 | - [Text field](docs/examples/text-field.md)
195 | - [Number field](docs/examples/number-field.md)
196 | - [Slider top position](docs/examples/slider-top-position.md)
197 | - [Slider with background option](docs/examples/slider-with-background-ui-option.md)
198 | - [Dropdown UI](docs/examples/dropdown-ui.md)
199 | - [Multiple UI on different fields](docs/examples/multiple-ui-on-different-fields.md)
200 | - [Hide specific locale for a single field](docs/examples/hide-specific-locale-for-a-single-field.md)
201 | - [Locale not editable for a specific field](docs/examples/locale-not-editable-for-a-specific-field.md)
202 | - [Conditionally set a locale visible or not editable](docs/examples/conditionally-set-a-locale-visible-or-not-editable.md)
203 | - [List of values](docs/examples/list-of-values.md)
204 | - [Custom rows for i18n.text](docs/examples//custom-rows-for-i18ntext.md)
205 | - [Global validation](docs/examples/global-validation.md)
206 | - [Children validation](docs/examples/children-validation.md)
207 | - [Alternative locale label](docs/examples/alternative-locale-label.md)
208 | - [Alternative locale label 2](docs/examples//alternative-locale-label-2.md)
209 | - [Customized prefix](docs/examples//customized-prefix.md)
210 |
211 |
212 | ## 👀 Future features
213 | - New Sanity default types (boolean, date...)
214 | - Filters
215 | - Show all locales without slider/dropdown
216 | - Show only fulfilled translations
217 | - Show only empty translations
218 | - AI integration? 🤔
219 | - ...
220 |
221 |
222 | > While writing this documentation, I realized that with the 'prefix' option, you can define the plugin multiple times with different prefixes.
223 | >
224 | > Codes and Labels are customizable, and this plugin could be used for other use cases, not only for internationalization.
225 | >
226 | > So, perhaps the name 'I18N Fields' is already outdated? 😅 Should I find a different name? Any suggestions? 😂
227 |
228 |
234 |
235 | ## 🧪 Develop & test
236 |
237 | This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit)
238 | with default configuration for build & watch scripts.
239 |
240 | See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio)
241 | on how to run this plugin with hotreload in the studio.
242 |
243 |
244 | ### Release new version
245 |
246 | Run ["CI & Release" workflow](https://github.com/williamiommi/sanity-plugin-i18n-fields/actions/workflows/main.yml).
247 | Make sure to select the main branch and check "Release new version".
248 |
249 | Semantic release will only release on configured branches, so it is safe to run release on any branch.
250 |
--------------------------------------------------------------------------------