intlify / bundle-tools

bundling for intlify i18n tools
MIT License
239 stars 37 forks source link

@intlify/vite-plugin-vue-i18n locale translation files splitting #97

Open volarname opened 2 years ago

volarname commented 2 years ago

Clear and concise description of the problem

Suggested solution

locales/fr/group1/subject1.yaml locales/fr/group1/subject2.yaml locales/fr/group2/subject3.yaml

There are to possibilities how to achieve this:
1. more complex way - first directory will define the locale and each directory is a subject path, so if `locales/en/group1/subject1.yaml` content is this:

foo: foo bar: baz: baz

the "generated" file for en locale will be:

group1: subject1: foo: foo bar: baz: baz

2. more simple way - just merge the content, use root subject as locale - we will need to define whole subject in each file, the plugin will just merge everything in one
`locales/en/group1/subject1.yaml`

en: group1: subject1: foo: foo bar: baz: baz

`locales/fr/group1/subject1.yaml`

fr: group1: subject1: foo: foo bar: baz: baz



### Alternative

If there is any existing way how to achieve similar code spliting right now without need to implement it, please add it to documentation, because now there is nothing about it.

### Additional context

I tried several ways how to structure the dirs, files and content of yaml/json files and nothing was correctly merged, always only one file worked and other were ignored.

### Validations

- [X] Read the [Contributing Guidelines](https://github.com/intlify/bundle-tools/blob/main/.github/contributing.md).
- [X] Read the README
- [X] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
AngadSethi commented 2 years ago

Would love to have support for this! Currently, I'm having to cram all my i18n strings into one JSON file to make it work, and that route is really not scalable.

volarname commented 2 years ago

i made for now my custom load for this. for now without dynamic import (all languages and all files are imported) vite is a requirement for this (glob import and load of yaml, but probably can be replaced by some library for webpack):

// @/locales/index.js
// note: now import is not dynamic, so all language files are loaded at once
const modules = import.meta.globEager('./(sk|en)/**/*.yaml', { assert: { type: 'yaml' } })

const setValueByPath = (obj: any, path: string, value: any, splitChar = '.') => {
  const a = path.split(splitChar)
  let o = obj
  while (a.length - 1) {
    const n = a.shift()
    if (isUndefined(n)) return
    if (!(n in o)) o[n] = {}
    o = o[n]
  }
  o[a[0]] = value
}

const cleanupKey = (key: string) => {
  key = key.substring(2)
  return key.substring(0, key.indexOf('.'))
}

const final = {}
for (const key in modules) {
  const path = cleanupKey(key)
  setValueByPath(final, path, modules[key].default, '/')
}

export const messages = final

// @/utils/object.js

then just import messages to i18n config. then file with this path @/locales/en/blog/article.json with content (note first folder is lang code):

field:
  title: Title
  body: Bodytext
button:
  save: Save

will be transformed to these keys under en language, for example:

const { t } = useI18n({ useScope: 'global' })

t('blog.article.field.title')
t('blog.article.field.body')
t('blog.article.button.save')

i think this can work with json also. i still prefer a solution from authors, for now i can have this as temp solution. just wondering this is still not implemented as a base feature

kazupon commented 2 years ago

Yeah, import.meta.globEager (import.meta.glob) is useful. I would consider implementing it, but it's a proprietary feature that is not standardized in ECMAScript (tc39) or elsewhere. Since this plugin is for Vite, I would be willing to provide the functionality, but I'm a bit reluctant to rely on a non-standardized feature to implement it 😅

logue commented 2 years ago

The yaml parser for import.meta.glob behaves poorly.

Specifically, the following YAML fails to parse (checked with YAML Lint and eslint-plugin-yaml):

some-messages: 
  - first message
  - second message

Therefore, I implemented parsing with js-yaml.

import yaml from 'js-yaml';

/** Return value */
const entries: any = {};

// Get locale yaml file.
Object.entries(
  import.meta.glob('./(en|ja)/**/*.yml', {
    eager: true,
    as: 'raw',
  })
).forEach(module => {
  /** File Path */
  const path = module[0];
  /** Locale */
  const locale = path.slice(2, 4);
  /** Key name  */
  const key = path.slice(5, -4).replace(/\//, '.');
  /** Yaml value */
  const value = yaml.load(module[1]);

  if (key === 'index') {
    // Set index.yml to root.
    entries[locale] = { ...entries[locale], ...value };
  } else {
    // Otherwise, assign to child object
    const entry = {};
    entry[key] = value;
    entries[locale] = { ...entries[locale], ...entry };
  }
});

export const messages = entries;

index.yml is assigned to root.

It's not very clean code, but I think it will work.

logue commented 2 years ago

The reason was that Vite does not include a yaml parser. https://github.com/vitejs/vite/issues/10318

velly commented 1 year ago

I use ts and vite import to manage all translation files. So, I can do a lots of things.

//configuration/locale.ts export const supportedLocales = { en: { name: "English" }, "zh-CN": { name: "中文简体" }, "zh-TW": { name: "中文繁體" }, };

export const defaultLocale = "en";


//locale/index.ts import { supportedLocales } from "@/configuration/locale"; import { merge } from "lodash";

const autoImportedLangs: Record<string, () => Promise> = import.meta.glob( ["./*.ts", "/src/components/*/lang/.ts"], { import: "default", eager: true, } );

for (const path in autoImportedLangs) { const lang: string = path.substring( path.lastIndexOf("/") + 1, path.lastIndexOf(".") ); if (lang in usedLangs) { merge(usedLangs[lang], autoImportedLangs[path]); } }

export default usedLangs;


install the plugin:

import { createI18n } from "vue-i18n"; import { defaultLocale } from "@/configuration/locale"; import messages from "@/locales";

export default createI18n({ locale: defaultLocale, fallbackWarn: false, missingWarn: false, legacy: false, fallbackLocale: defaultLocale, messages, });