Skip to content

Make field label and default options localizable #403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
servedsmart opened this issue May 20, 2025 · 7 comments
Open

Make field label and default options localizable #403

servedsmart opened this issue May 20, 2025 · 7 comments

Comments

@servedsmart
Copy link

servedsmart commented May 20, 2025

Hello,

I have noticed that it isn't possible to localize a lot of the entries in the configuration. I would mainly use this for collections, but it might also be feasible to implement this for other config options.

I tried using an approach layed out here, I'm not sure if the same behaviour can be observed in Decap CMS, but when opening Sveltia I just get a list of the entries in collections with the different localized labels. Switching the locale through the select widget will also not change the label of specified fields. As far as I know, there is no other option that would enable users to customize that behaviour or other config options.

Since sveltia already has a language switcher button in the editor if you use i18n: true, I'd suggest adding logic that allows a user to add localized entries to fields in their collections or other config keys.

This is an example of how hugo does this. It should be possible to implement a similar approach here.

This is my current auto-generated configuration for reference.

@kyoshino
Copy link
Member

kyoshino commented May 20, 2025

Sveltia already implements first-class i18n support on top of the Decap CMS i18n support.

Please set up i18n with Decap CMS first and migrate to Sveltia CMS. Then let me know if something is not working. I’m happy to provide help with features specific to Sveltia CMS, but I can’t help set up basic i18n configuration.

@servedsmart
Copy link
Author

servedsmart commented May 20, 2025

Hi, i18n works but you can't modify the defaults or labels of fields as far as I know. That would be useful if someone has spcific tags/categories or different labels for each language. Therefore I wanted to use a different approach. Just using i18n works but has some drawbacks.

I at least didn't find any documentation on how one would implement that with sveltia's i18n

@kyoshino
Copy link
Member

Maybe I now understand what you want.

If you want to localize tags/categories, create a separate collection for these tags/categories, and use the relation widget to refer to it, instead of using the list field and hardcoding defaults in the configuration.

Unfortunately, localizing the label option for a field/collection is not possible at this time.

@servedsmart
Copy link
Author

servedsmart commented May 20, 2025

Maybe I now understand what you want.

If you want to localize tags/categories, create a separate collection for these tags/categories, and use the relation widget to refer to it, instead of using the list field and hardcoding defaults in the configuration.

Unfortunately, localizing the label option for a field/collection is not possible at this time.

Thank you very much. That sounds like a great idea. It at least solves my current problem since localizing the labels is kind of optional.

I'd however recommend leaving this issue open since this feature would in my opinion be quite helpful. The approach that Hugo uses should solve this without any problems. It could enable localizing labels and allow configuring defaults without using the relation widget.

@kyoshino
Copy link
Member

kyoshino commented May 20, 2025

I said I can’t help but here’s what I’ve got:

Sample configuration
{
  config: {
    load_config_file: false,
    backend: {
      // base_url: 'https://sveltia-cms-auth.servedsmart-6knt8ma96knt8ma9.workers.dev',
      // branch: 'cms',
      // name: 'github',
      // repo: 'servedsmart/servedsmart.top',
      name: 'test-repo',
    },
    collections: [
      {
        create: true,
        i18n: true,
        editor: { preview: false },
        fields: [
          {
            name: 'label',
            label: 'Label',
            i18n: true,
          },
        ],
        slug: '{{uuid_short}}',
        folder: 'content/tags',
        format: 'toml-frontmatter',
        label: 'Tags',
        name: 'tags',
        path: '{{slug}}/index',
      },
      {
        create: true,
        i18n: true,
        editor: { preview: false },
        fields: [
          {
            name: 'label',
            label: 'Label',
            i18n: true,
          },
        ],
        slug: '{{uuid_short}}',
        folder: 'content/categories',
        format: 'toml-frontmatter',
        label: 'Categories',
        name: 'categories',
        path: '{{slug}}/index',
      },
      {
        create: true,
        i18n: true,
        editor: { preview: true },
        slug: '{{title | localize}}',
        fields: [
          {
            label: 'Title',
            name: 'title',
            widget: 'string',
            i18n: true,
          },
          {
            default: '{{now}}',
            format: 'YYYY-MM-DDTHH:mm:ssZ',
            label: 'Date',
            name: 'date',
            picker_utc: true,
            widget: 'datetime',
            i18n: 'duplicate',
          },
          {
            label: 'Description',
            name: 'description',
            required: false,
            widget: 'string',
            i18n: true,
          },
          {
            label: 'Summary',
            name: 'summary',
            required: false,
            widget: 'string',
            i18n: true,
          },
          {
            label: 'Categories',
            name: 'categories',
            required: false,
            multiple: true,
            widget: 'relation',
            collection: 'categories',
            value_field: '{{slug}}',
            display_fields: ['label'],
            i18n: 'duplicate',
          },
          {
            label: 'Tags',
            name: 'tags',
            required: false,
            multiple: true,
            widget: 'relation',
            collection: 'tags',
            value_field: '{{slug}}',
            display_fields: ['label'],
            i18n: 'duplicate',
          },
          {
            default: false,
            label: 'Draft',
            name: 'draft',
            widget: 'boolean',
            i18n: 'duplicate',
          },
          {
            label: 'Body',
            modes: ['raw', 'rich_text'],
            name: 'body',
            required: false,
            sanitize_preview: true,
            widget: 'markdown',
            i18n: true,
          },
        ],
        folder: 'content/employees',
        format: 'toml-frontmatter',
        index_file: { name: '_index' },
        label: 'Employees',
        media_folder: '',
        name: 'employees',
        path: '{{slug}}/index',
        public_folder: '',
      },
      {
        create: true,
        i18n: true,
        editor: { preview: true },
        slug: '{{title | localize}}',
        fields: [
          {
            label: 'Title',
            name: 'title',
            widget: 'string',
            i18n: true,
          },
          {
            default: '{{now}}',
            format: 'YYYY-MM-DDTHH:mm:ssZ',
            label: 'Date',
            name: 'date',
            picker_utc: true,
            widget: 'datetime',
            i18n: 'duplicate',
          },
          {
            label: 'Description',
            name: 'description',
            required: false,
            widget: 'string',
            i18n: true,
          },
          {
            label: 'Summary',
            name: 'summary',
            required: false,
            widget: 'string',
            i18n: true,
          },
          {
            label: 'Categories',
            name: 'categories',
            required: false,
            multiple: true,
            widget: 'relation',
            collection: 'categories',
            value_field: '{{slug}}',
            display_fields: ['label'],
            i18n: 'duplicate',
          },
          {
            label: 'Tags',
            name: 'tags',
            required: false,
            multiple: true,
            widget: 'relation',
            collection: 'tags',
            value_field: '{{slug}}',
            display_fields: ['label'],
            i18n: 'duplicate',
          },
          {
            default: false,
            label: 'Draft',
            name: 'draft',
            widget: 'boolean',
            i18n: 'duplicate',
          },
          {
            label: 'Body',
            modes: ['raw', 'rich_text'],
            name: 'body',
            required: false,
            sanitize_preview: true,
            widget: 'markdown',
            i18n: true,
          },
        ],
        folder: 'content/posts',
        format: 'toml-frontmatter',
        index_file: { name: '_index' },
        label: 'Posts',
        media_folder: '',
        name: 'posts',
        path: '{{slug}}/index',
        public_folder: '',
      },
      {
        create: true,
        i18n: true,
        editor: { preview: true },
        slug: '{{title | localize}}',
        fields: [
          {
            label: 'Title',
            name: 'title',
            widget: 'string',
            i18n: true,
          },
          {
            default: '{{now}}',
            format: 'YYYY-MM-DDTHH:mm:ssZ',
            label: 'Date',
            name: 'date',
            picker_utc: true,
            widget: 'datetime',
            i18n: 'duplicate',
          },
          {
            label: 'Description',
            name: 'description',
            required: false,
            widget: 'string',
            i18n: true,
          },
          {
            label: 'Summary',
            name: 'summary',
            required: false,
            widget: 'string',
            i18n: true,
          },
          {
            label: 'Categories',
            name: 'categories',
            required: false,
            multiple: true,
            widget: 'relation',
            collection: 'categories',
            value_field: '{{slug}}',
            display_fields: ['label'],
            i18n: 'duplicate',
          },
          {
            label: 'Tags',
            name: 'tags',
            required: false,
            multiple: true,
            widget: 'relation',
            collection: 'tags',
            value_field: '{{slug}}',
            display_fields: ['label'],
            i18n: 'duplicate',
          },
          {
            default: false,
            label: 'Draft',
            name: 'draft',
            widget: 'boolean',
            i18n: 'duplicate',
          },
          {
            label: 'Body',
            modes: ['raw', 'rich_text'],
            name: 'body',
            required: false,
            sanitize_preview: true,
            widget: 'markdown',
            i18n: true,
          },
        ],
        folder: 'content/services',
        format: 'toml-frontmatter',
        index_file: { name: '_index' },
        label: 'Services',
        media_folder: '',
        name: 'services',
        path: '{{slug}}/index',
        public_folder: '',
      },
      {
        create: true,
        i18n: true,
        editor: { preview: true },
        slug: '{{title | localize}}',
        fields: [
          {
            label: 'Title',
            name: 'title',
            widget: 'string',
            i18n: true,
          },
          {
            default: '{{now}}',
            format: 'YYYY-MM-DDTHH:mm:ssZ',
            label: 'Date',
            name: 'date',
            picker_utc: true,
            widget: 'datetime',
            i18n: 'duplicate',
          },
          {
            label: 'Description',
            name: 'description',
            required: false,
            widget: 'string',
            i18n: true,
          },
          {
            label: 'Summary',
            name: 'summary',
            required: false,
            widget: 'string',
            i18n: true,
          },
          {
            label: 'Categories',
            name: 'categories',
            required: false,
            multiple: true,
            widget: 'relation',
            collection: 'categories',
            value_field: '{{slug}}',
            display_fields: ['label'],
            i18n: 'duplicate',
          },
          {
            label: 'Tags',
            name: 'tags',
            required: false,
            multiple: true,
            widget: 'relation',
            collection: 'tags',
            value_field: '{{slug}}',
            display_fields: ['label'],
            i18n: 'duplicate',
          },
          {
            default: false,
            label: 'Draft',
            name: 'draft',
            widget: 'boolean',
            i18n: 'duplicate',
          },
          {
            label: 'Body',
            modes: ['raw', 'rich_text'],
            name: 'body',
            required: false,
            sanitize_preview: true,
            widget: 'markdown',
            i18n: true,
          },
        ],
        folder: 'content/terms',
        format: 'toml-frontmatter',
        index_file: { name: '_index' },
        label: 'Terms',
        media_folder: '',
        name: 'terms',
        path: '{{slug}}/index',
        public_folder: '',
      },
    ],
    i18n: { default_locale: 'de', locales: ['de', 'en'], structure: 'multiple_files' },
    media_folder: 'static/img',
    media_libraries: {
      default: {
        config: {
          transformations: {
            raster_image: { format: 'webp', height: 2048, quality: 85, width: 2048 },
            svg: { optimize: true },
          },
        },
      },
    },
    public_folder: '/img',
    publish_mode: 'simple',
    show_preview_links: false,
    site_url: 'https://cms.servedsmart.top/',
    slug: { clean_accents: true, encoding: 'ascii', sanitize_replacement: '_' },
  },
}
  • Don’t repeat the same collection for each language
  • Use i18n: 'duplicate' wisely
  • Save slugs instead of hardcoded tags/categories, because these labels could be changed later; how to ”decode” these slugs depends on the framework

I believe defaults, especially for tags/categories that can be added/renamed later, should never be hardcoded within the configuration. But yes, hypothetically I could make label and default localizable like below. I’ll check back later.

{
  "widget": "string",
  "name": "description",
  "label": { "en": "Description", "de": "Beschreibung" },
},
{
  "widget": "list",
  "name": "categories",
  "label": { "en": "Categories", "de": "Kategorien" },
  "default": {
    "en": [
      "car repair",
      "tuning",
      "team",
      "internal",
      "legal",
      "announcement",
      "service"
    ],
    "de": [
      "autoreparatur",
      "tuning",
      "mitarbeiter",
      "unternehmensintern",
      "rechtliches",
      "ankündigung",
      "dienstleistung"
    ],
  },
},

@kyoshino kyoshino changed the title Add a way to enable users to localize entries in their configuration Make field label and default options localizable May 20, 2025
@servedsmart
Copy link
Author

servedsmart commented May 20, 2025

Thank you very much for the sample configuration and the very detailed explaination! I will try to implement something similar. This should more than solve my initial problem 👍

Also thank you for thinking about if you could implement this in a similar way as you have mentioned in your second example code.

Regarding your point:

  • Save slugs instead of hardcoded tags/categories, because these labels could be changed later; how to ”decode” these slugs depends on the framework

The reason I am hardcoding them is to give people that don't really know what they would use as a tag/category a template. I use the same in hugo archetypes. This lets a user delete entries they don't deem fitting.

It's very possible that I just don't get it, but what exactly would a slug provide in this scenario? The tags/categories are used for SEO purposes and for letting users find related posts in the same category/tag. If I'd use a uuid slug instead, would that not break that functionality?
Also /tags gets generated by my theme (blowfish) or maybe even hugo itself. Therefore I also don't understand why you would specify content/tags as "folder":?
It might either be me just misunderstanding your config or my initial explaination not having provided enough explaination, but I think hardcoding here would be the only option. I generate my config from a bash script that gets the tags/categories from my hugo archetypes so it isn't necessarily that "hardcoded" anyways.

It would be awesome if I could somehow dynamically get the tags/categories from the site root /tags or /categories but if they are generated by hugo/the theme dynamically I'm not sure if that's possible since afaik the "folder": for sveltia uses the repo, not the site. I will do some research later and post my solution to this problem. I really hope that I didn't misunderstand your answer too much.

Edit

I couldn't implement this in a way that allowed me to get different tags/categories for each language in the ui. I'm not sure if that was what the sample configuration was intended to do, but this is the only way implementing defaults for tags/categories would make sense. Currently I only get the slugs/uuids in the ui. If they are meant to be decoded by hugo, I don't think that at least in my current configuration hugo would try to decode them.

This is my current web ui for reference when trying to set tags/categories:
Image

It is very possible that this comes down to user error since I am very new to sveltia-cms and have never used decap cms before and generally never really did a lot of web "development" until a bit more than a month ago. However I think that the possible future solution you mentioned would in any scenario make this less confusing and way easier to implement.

@kyoshino
Copy link
Member

kyoshino commented May 20, 2025

You can of course use localized tags/categories for links on your site for SEO purposes. But within data, it’s always better to use a permanent value when connecting different collections using a relation field. UUID slugs won’t change from time to time, so it’s suitable for value_field.

Hardcoding makes things just difficult; what if you want to change “car repair” to “auto repair” later? The latter is common here in Canada. It can be done easily with a relation field connected with a UUID slug, but you have to rewrite many entries if tags/categories are hardcoded.

I’m not familiar with the structure of Hugo as I’m no longer an active user of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants