Skip to content

Commit 058af85

Browse files
committed
chore: migrate strapi-plugin-language to Strapi 5
1 parent aaae58f commit 058af85

30 files changed

+2197
-74
lines changed

.changeset/bumpy-rats-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@frameless/strapi-plugin-language": major
3+
---
4+
5+
Migrate strapi-plugin-language to Strapi 5

apps/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@_sh/strapi-plugin-ckeditor": "6.0.3",
2424
"@frameless/preview-button": "workspace:*",
2525
"@frameless/strapi-plugin-env-label": "workspace:*",
26+
"@frameless/strapi-plugin-language": "workspace:*",
2627
"@strapi/design-system": "2.1.2",
2728
"@strapi/plugin-graphql": "5.33.4",
2829
"@strapi/plugin-users-permissions": "5.33.4",

apps/dashboard/src/components/components/utrecht-link.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
"bijzonderheden",
3434
"contact"
3535
]
36+
},
37+
"language": {
38+
"type": "customField",
39+
"customField": "plugin::language.language",
40+
"options": {
41+
"defaultLanguage": "nl"
42+
}
3643
}
3744
}
3845
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# @frameless/strapi-plugin-language 1.0.0 (2024-06-10)
2+
3+
## 0.0.0
4+
5+
### Minor Changes
6+
7+
- 82fa577: Wanneer een linktekst in een andere taal is geschreven, dan kun je nu in Strapi de taal instellen. Als je dit doet verbeter je de toegankelijkheid, want dan kan de tekst met de juiste taal voorgelezen worden.
8+
9+
### Features
10+
11+
- **strapi-plugin-language:** create strapi language plugin ([a6653d3](https://github.com/frameless/strapi/commit/a6653d37ede5d8300b7a10d6f70ceb12fdfa0703))
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Strapi Plugin Language
2+
3+
A **Strapi custom field** that provides a dropdown selector for languages.
4+
The field stores values as **ISO 639-1 language codes**, making it easy to integrate with APIs, localization logic, or external services.
5+
6+
Currently supported languages:
7+
8+
- English (`en`)
9+
- Dutch (`nl`)
10+
- Arabic (`ar`)
11+
- Ukrainian (`uk`)
12+
- Turkish (`tr`)
13+
14+
This plugin is compatible with **Strapi v5 and above**.
15+
16+
---
17+
18+
## Features
19+
20+
- ✅ Custom field with a language dropdown
21+
- ✅ Returns standardized ISO 639-1 language codes
22+
- ✅ Configurable default language
23+
- ✅ Works with any content type
24+
- ✅ Fully compatible with Strapi `>= 5.0.0`
25+
26+
---
27+
28+
## Requirements
29+
30+
- **Strapi**: `>= 5.0.0`
31+
- **Node.js**: `>= 22.0.0 < 25`
32+
- **pnpm**: `>= 10.0.0`
33+
34+
```json
35+
36+
"engines": {
37+
"node": ">=22.0.0 <25",
38+
"pnpm": ">=10.0.0"
39+
},
40+
```
41+
42+
## Usage
43+
44+
Once the plugin is installed and the Strapi admin panel has been rebuilt, the **Language** custom field becomes available in the Content-Type Builder.
45+
46+
### Adding the field to a content type
47+
48+
1. Open the **Strapi Admin Dashboard**
49+
2. Navigate to **Content-Type Builder**
50+
3. Create a new content type or edit an existing one
51+
4. Click **Add another field**
52+
5. Switch to the **Custom** tab
53+
6. Select **Language** from the list of available custom fields
54+
7. Configure the field settings and save
55+
56+
---
57+
58+
### Field settings
59+
60+
The Language custom field supports both **basic** and **advanced** configuration options.
61+
62+
#### Advanced settings
63+
64+
- **Default language**
65+
Allows you to select a language that will be pre-selected when creating new entries.
66+
67+
---
68+
69+
### Stored value
70+
71+
The field stores and returns values using **ISO 639-1 language codes**.
72+
73+
Example API response:
74+
75+
```json
76+
{
77+
"language": "en"
78+
}
79+
80+
```
81+
82+
## License
83+
84+
This project is licensed under the **European Union Public Licence (EUPL) v1.2**.
85+
86+
See the [LICENSE.md](../../LICENSE.md) file at the root of the monorepo for full license text.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import i18nLanguages from '@cospired/i18n-iso-languages';
2+
import { Combobox, ComboboxOption, Field } from '@strapi/design-system';
3+
import enJson from '@cospired/i18n-iso-languages/langs/en.json';
4+
import nlJson from '@cospired/i18n-iso-languages/langs/nl.json';
5+
6+
import { getLanguageOptions, languageCode } from '../../utils/getLanguageOptions';
7+
import { useContentLocale } from '../../hooks/useContentLocale';
8+
9+
i18nLanguages.registerLocale(enJson);
10+
i18nLanguages.registerLocale(nlJson);
11+
12+
export interface LanguageFieldFieldInputProps {
13+
attribute: {
14+
type: string;
15+
customField?: string;
16+
options?: {
17+
defaultLanguage?: string;
18+
};
19+
[key: string]: unknown;
20+
};
21+
disabled: boolean;
22+
error?: string;
23+
rawError?: unknown;
24+
hint?: string;
25+
label: string;
26+
placeholder?: string;
27+
name: string;
28+
required: boolean;
29+
unique: boolean;
30+
type: string;
31+
value: string | null;
32+
initialValue?: string;
33+
mainField?: string;
34+
35+
onChange: (event: unknown) => void;
36+
onBlur: () => void;
37+
onFocus: () => void;
38+
}
39+
40+
export const LanguageField = ({
41+
value,
42+
onChange,
43+
name,
44+
required,
45+
attribute,
46+
type,
47+
placeholder,
48+
disabled,
49+
error,
50+
label,
51+
hint,
52+
}: LanguageFieldFieldInputProps) => {
53+
const locale = useContentLocale();
54+
const languageOptions = getLanguageOptions(languageCode, locale ?? 'nl');
55+
const defaultLanguage = attribute.options?.defaultLanguage;
56+
57+
return (
58+
<Field.Root name={name} id={name} error={error} hint={hint}>
59+
<Field.Label>{label}</Field.Label>
60+
<Combobox
61+
placeholder={placeholder}
62+
aria-label={label}
63+
aria-disabled={disabled}
64+
disabled={disabled}
65+
required={required}
66+
value={value || defaultLanguage}
67+
onChange={(code: string) => onChange({ target: { name, value: code, type } })}
68+
>
69+
{languageOptions.map(({ code, name }) => (
70+
<ComboboxOption value={code} key={code}>
71+
{name}
72+
</ComboboxOption>
73+
))}
74+
</Combobox>
75+
<Field.Hint />
76+
<Field.Error />
77+
</Field.Root>
78+
);
79+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Flex } from '@strapi/design-system';
2+
import { Earth } from '@strapi/icons';
3+
4+
export const LanguageFieldIcon = () => {
5+
return (
6+
<Flex justifyContent="center" alignItems="center" width={7} height={6} hasRadius aria-hidden>
7+
<Earth fill="primary600" />
8+
</Flex>
9+
);
10+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useEffect, useState } from 'react';
2+
3+
import { getCurrentContentLocale } from '../utils/getCurrentContentLocale';
4+
5+
export const useContentLocale = () => {
6+
const [locale, setLocale] = useState<string | null>(null);
7+
8+
useEffect(() => {
9+
const update = () => {
10+
setLocale(getCurrentContentLocale());
11+
};
12+
13+
update(); // initialize on mount
14+
window.addEventListener('popstate', update);
15+
16+
return () => window.removeEventListener('popstate', update);
17+
}, []);
18+
19+
return locale;
20+
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { getTranslation } from './utils/getTranslation';
2+
import { LanguageFieldIcon } from './components/LanguageFieldIcon';
3+
import { PLUGIN_ID } from './pluginId';
4+
import { getLanguageOptions, languageCode } from './utils/getLanguageOptions';
5+
import { getCurrentContentLocale } from './utils/getCurrentContentLocale';
6+
7+
type TranslateOptions = Record<string, string>;
8+
9+
const prefixPluginTranslations = (translate: TranslateOptions, pluginId: string): TranslateOptions => {
10+
if (!pluginId) {
11+
throw new TypeError("pluginId can't be empty");
12+
}
13+
return Object.keys(translate).reduce((acc, current) => {
14+
acc[`${pluginId}.${current}`] = translate[current];
15+
return acc;
16+
}, {} as TranslateOptions);
17+
};
18+
19+
export default {
20+
register(app: any) {
21+
const locale = getCurrentContentLocale() || 'nl';
22+
const optionsForStrapi = getLanguageOptions(languageCode, locale).map((lang) => ({
23+
key: lang.code,
24+
value: lang.code,
25+
metadatas: {
26+
intlLabel: {
27+
id: `${PLUGIN_ID}.${lang.code}`,
28+
defaultMessage: lang.name,
29+
},
30+
},
31+
}));
32+
app.customFields.register({
33+
name: PLUGIN_ID,
34+
pluginId: PLUGIN_ID,
35+
type: 'string',
36+
icon: LanguageFieldIcon,
37+
intlLabel: {
38+
id: getTranslation(`${PLUGIN_ID}.label`),
39+
defaultMessage: 'Select language',
40+
},
41+
intlDescription: {
42+
id: getTranslation(`${PLUGIN_ID}.description`),
43+
defaultMessage: 'Select language',
44+
},
45+
components: {
46+
Input: async () =>
47+
import('./components/LanguageField').then((module) => ({
48+
default: module.LanguageField,
49+
})),
50+
},
51+
options: {
52+
advanced: [
53+
{
54+
sectionTitle: {
55+
id: 'global.settings',
56+
defaultMessage: 'Settings',
57+
},
58+
items: [
59+
{
60+
name: 'required',
61+
type: 'checkbox',
62+
intlLabel: {
63+
id: 'form.attribute.item.requiredField',
64+
defaultMessage: 'Required field',
65+
},
66+
description: {
67+
id: 'form.attribute.item.requiredField.description',
68+
defaultMessage: "You won't be able to create an entry if this field is empty",
69+
},
70+
},
71+
72+
{
73+
intlLabel: {
74+
id: 'my-plugin.settings.defaultLanguage',
75+
defaultMessage: 'Default language',
76+
},
77+
name: 'options.defaultLanguage',
78+
type: 'select',
79+
options: optionsForStrapi,
80+
},
81+
],
82+
},
83+
],
84+
},
85+
});
86+
},
87+
async registerTrads({ locales }: { locales: string[] }) {
88+
const importedTranslations = await Promise.all(
89+
locales.map((locale: any) => {
90+
return import(`./translations/${locale}.json`)
91+
.then(({ default: data }) => {
92+
return {
93+
data: prefixPluginTranslations(data, PLUGIN_ID),
94+
locale,
95+
};
96+
})
97+
.catch(() => {
98+
return {
99+
data: {},
100+
locale,
101+
};
102+
});
103+
}),
104+
);
105+
106+
return Promise.resolve(importedTranslations);
107+
},
108+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const PLUGIN_ID = 'language';

0 commit comments

Comments
 (0)