Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bumpy-rats-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@frameless/strapi-plugin-language": major
---

Migrate strapi-plugin-language to Strapi 5
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@_sh/strapi-plugin-ckeditor": "6.0.3",
"@frameless/preview-button": "workspace:*",
"@frameless/strapi-plugin-env-label": "workspace:*",
"@frameless/strapi-plugin-language": "workspace:*",
"@strapi/design-system": "2.1.2",
"@strapi/plugin-graphql": "5.33.4",
"@strapi/plugin-users-permissions": "5.33.4",
Expand Down
7 changes: 7 additions & 0 deletions apps/dashboard/src/components/components/utrecht-link.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
"bijzonderheden",
"contact"
]
},
"language": {
"type": "customField",
"customField": "plugin::language.language",
"options": {
"defaultLanguage": "nl"
}
}
}
}
11 changes: 11 additions & 0 deletions packages/strapi-plugin-language/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# @frameless/strapi-plugin-language 1.0.0 (2024-06-10)

## 0.0.0

### Minor Changes

- 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.

### Features

- **strapi-plugin-language:** create strapi language plugin ([a6653d3](https://github.com/frameless/strapi/commit/a6653d37ede5d8300b7a10d6f70ceb12fdfa0703))
86 changes: 86 additions & 0 deletions packages/strapi-plugin-language/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Strapi Plugin Language

A **Strapi custom field** that provides a dropdown selector for languages.
The field stores values as **ISO 639-1 language codes**, making it easy to integrate with APIs, localization logic, or external services.

Currently supported languages:

- English (`en`)
- Dutch (`nl`)
- Arabic (`ar`)
- Ukrainian (`uk`)
- Turkish (`tr`)

This plugin is compatible with **Strapi v5 and above**.

---

## Features

- ✅ Custom field with a language dropdown
- ✅ Returns standardized ISO 639-1 language codes
- ✅ Configurable default language
- ✅ Works with any content type
- ✅ Fully compatible with Strapi `>= 5.0.0`

---

## Requirements

- **Strapi**: `>= 5.0.0`
- **Node.js**: `>= 22.0.0 < 25`
- **pnpm**: `>= 10.0.0`

```json

"engines": {
"node": ">=22.0.0 <25",
"pnpm": ">=10.0.0"
},
```

## Usage

Once the plugin is installed and the Strapi admin panel has been rebuilt, the **Language** custom field becomes available in the Content-Type Builder.

### Adding the field to a content type

1. Open the **Strapi Admin Dashboard**
2. Navigate to **Content-Type Builder**
3. Create a new content type or edit an existing one
4. Click **Add another field**
5. Switch to the **Custom** tab
6. Select **Language** from the list of available custom fields
7. Configure the field settings and save

---

### Field settings

The Language custom field supports both **basic** and **advanced** configuration options.

#### Advanced settings

- **Default language**
Allows you to select a language that will be pre-selected when creating new entries.

---

### Stored value

The field stores and returns values using **ISO 639-1 language codes**.

Example API response:

```json
{
"language": "en"
}

```

## License

This project is licensed under the **European Union Public Licence (EUPL) v1.2**.

See the [LICENSE.md](../../LICENSE.md) file at the root of the monorepo for full license text.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import i18nLanguages from '@cospired/i18n-iso-languages';
import { Combobox, ComboboxOption, Field } from '@strapi/design-system';
import enJson from '@cospired/i18n-iso-languages/langs/en.json';
import nlJson from '@cospired/i18n-iso-languages/langs/nl.json';

import { getLanguageOptions, languageCode } from '../../utils/getLanguageOptions';
import { useContentLocale } from '../../hooks/useContentLocale';

i18nLanguages.registerLocale(enJson);
i18nLanguages.registerLocale(nlJson);

export interface LanguageFieldFieldInputProps {
attribute: {
type: string;
customField?: string;
options?: {
defaultLanguage?: string;
};
[key: string]: unknown;
};
disabled: boolean;
error?: string;
rawError?: unknown;
hint?: string;
label: string;
placeholder?: string;
name: string;
required: boolean;
unique: boolean;
type: string;
value: string | null;
initialValue?: string;
mainField?: string;

onChange: (event: unknown) => void;
onBlur: () => void;
onFocus: () => void;
}

export const LanguageField = ({
value,
onChange,
name,
required,
attribute,
type,
placeholder,
disabled,
error,
label,
hint,
}: LanguageFieldFieldInputProps) => {
const locale = useContentLocale();
const languageOptions = getLanguageOptions(languageCode, locale ?? 'nl');
const defaultLanguage = attribute.options?.defaultLanguage;

return (
<Field.Root name={name} id={name} error={error} hint={hint}>
<Field.Label>{label}</Field.Label>
<Combobox
placeholder={placeholder}
aria-label={label}
aria-disabled={disabled}
disabled={disabled}
required={required}
value={value || defaultLanguage}
onChange={(code: string) => onChange({ target: { name, value: code, type } })}
>
{languageOptions.map(({ code, name }) => (
<ComboboxOption value={code} key={code}>
{name}
</ComboboxOption>
))}
</Combobox>
<Field.Hint />
<Field.Error />
</Field.Root>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Flex } from '@strapi/design-system';
import { Earth } from '@strapi/icons';

export const LanguageFieldIcon = () => {
return (
<Flex justifyContent="center" alignItems="center" width={7} height={6} hasRadius aria-hidden>
<Earth fill="primary600" />
</Flex>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';

import { getCurrentContentLocale } from '../utils/getCurrentContentLocale';

export const useContentLocale = () => {
const [locale, setLocale] = useState<string | null>(null);

useEffect(() => {
const update = () => {
setLocale(getCurrentContentLocale());
};

update(); // initialize on mount
window.addEventListener('popstate', update);

return () => window.removeEventListener('popstate', update);
}, []);

return locale;
};
108 changes: 108 additions & 0 deletions packages/strapi-plugin-language/admin/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { getTranslation } from './utils/getTranslation';
import { LanguageFieldIcon } from './components/LanguageFieldIcon';
import { PLUGIN_ID } from './pluginId';
import { getLanguageOptions, languageCode } from './utils/getLanguageOptions';
import { getCurrentContentLocale } from './utils/getCurrentContentLocale';

type TranslateOptions = Record<string, string>;

const prefixPluginTranslations = (translate: TranslateOptions, pluginId: string): TranslateOptions => {
if (!pluginId) {
throw new TypeError("pluginId can't be empty");
}
return Object.keys(translate).reduce((acc, current) => {
acc[`${pluginId}.${current}`] = translate[current];
return acc;
}, {} as TranslateOptions);
};

export default {
register(app: any) {
const locale = getCurrentContentLocale() || 'nl';
const optionsForStrapi = getLanguageOptions(languageCode, locale).map((lang) => ({
key: lang.code,
value: lang.code,
metadatas: {
intlLabel: {
id: `${PLUGIN_ID}.${lang.code}`,
defaultMessage: lang.name,
},
},
}));
app.customFields.register({
name: PLUGIN_ID,
pluginId: PLUGIN_ID,
type: 'string',
icon: LanguageFieldIcon,
intlLabel: {
id: getTranslation(`${PLUGIN_ID}.label`),
defaultMessage: 'Select language',
},
intlDescription: {
id: getTranslation(`${PLUGIN_ID}.description`),
defaultMessage: 'Select language',
},
components: {
Input: async () =>
import('./components/LanguageField').then((module) => ({
default: module.LanguageField,
})),
},
options: {
advanced: [
{
sectionTitle: {
id: 'global.settings',
defaultMessage: 'Settings',
},
items: [
{
name: 'required',
type: 'checkbox',
intlLabel: {
id: 'form.attribute.item.requiredField',
defaultMessage: 'Required field',
},
description: {
id: 'form.attribute.item.requiredField.description',
defaultMessage: "You won't be able to create an entry if this field is empty",
},
},

{
intlLabel: {
id: 'my-plugin.settings.defaultLanguage',
defaultMessage: 'Default language',
},
name: 'options.defaultLanguage',
type: 'select',
options: optionsForStrapi,
},
],
},
],
},
});
},
async registerTrads({ locales }: { locales: string[] }) {
const importedTranslations = await Promise.all(
locales.map((locale: any) => {
return import(`./translations/${locale}.json`)
.then(({ default: data }) => {
return {
data: prefixPluginTranslations(data, PLUGIN_ID),
locale,
};
})
.catch(() => {
return {
data: {},
locale,
};
});
}),
);

return Promise.resolve(importedTranslations);
},
};
1 change: 1 addition & 0 deletions packages/strapi-plugin-language/admin/src/pluginId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PLUGIN_ID = 'language';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"language.label": "Language",
"language.description": "Select a language"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"language.label": "Taal",
"language.description": "Kies een taal"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const getCurrentContentLocale = (): string | null => {
if (typeof window === 'undefined') return null;
const params = new URLSearchParams(window.location.search);
return params.get('plugins[i18n][locale]');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import i18nLanguages from '@cospired/i18n-iso-languages';
import enJson from '@cospired/i18n-iso-languages/langs/en.json';
import nlJson from '@cospired/i18n-iso-languages/langs/nl.json';

import { sortLanguagesAlphabetically } from './sortLanguagesAlphabetically';

i18nLanguages.registerLocale(enJson);
i18nLanguages.registerLocale(nlJson);

type LanguageOption = {
name: string;
code: string;
};

/**
* Returns an array of language options with names in the target locale.
* @param codes Array of ISO 639-1 language codes
* @param locale Display locale code (e.g., 'en', 'nl')
*/
export const getLanguageOptions = (codes: string[], locale: string): LanguageOption[] =>
sortLanguagesAlphabetically(
codes?.map((code) => ({
name: i18nLanguages.getName(code.toUpperCase(), locale) || code,
code: code.toLowerCase(),
})),
);

export const languageCode = ['en', 'nl', 'ar', 'uk', 'tr'];
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { PLUGIN_ID } from '../pluginId';

export const getTranslation = (id: string): string => `${PLUGIN_ID}.${id}`;
Loading