Repository: intlify/eslint-plugin-vue-i18n
Pull request #492 (2024-04-02 21:36:02)

feat: add `lowercase` to `key-format-style` rule
Our company-wide translation key policy calls for all lowercase keys. I opened an [issue](https://github.com/intlify/eslint-plugin-vue-i18n/issues/431) a while ago but never got any answers. Here’s my PR.

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 148d68303859408b54fe3d61e86a367b59f8c993

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/oikalyptus/eslint-plugin-vue-i18n/new/feature/add_lowercase_key_format_style?filename=.changeset/silver-toys-lick.md&value=---%0A%22%40intlify%2Feslint-plugin-vue-i18n%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20%60lowercase%60%20to%20%60key-format-style%60%20rule%0A)



Patch:
diff --git a/.changeset/modern-apricots-flash.md b/.changeset/modern-apricots-flash.md
new file mode 100644
index 0000000..1901cb4
--- /dev/null
+++ b/.changeset/modern-apricots-flash.md
@@ -0,0 +1,5 @@
+---
+'@intlify/eslint-plugin-vue-i18n': minor
+---
+
+add lowercase option to key-format-style rule
diff --git a/docs/rules/key-format-style.md b/docs/rules/key-format-style.md
index f038388..38eb8f5 100644
--- a/docs/rules/key-format-style.md
+++ b/docs/rules/key-format-style.md
@@ -13,6 +13,7 @@ This rule aims to enforces specific casing for localization key names.
 ```yaml
 camelCaseKey: The key for this value is camel case.
 kebab-case-key: The key for this value is kebab case.
+lowercase: The key for this value is lower case.
 snake_case_key: The key for this value is snake case.
 mixed_Case-key: Perhaps you don't want to use this casing.
 ```
@@ -47,7 +48,7 @@ Also, the following localization key definitions are reported as errors, because
 {
   "@intlify/vue-i18n/key-format-style": [
     "error",
-    "camelCase" | "kebab-case" | "snake_case",
+    "camelCase" | "kebab-case" | "lowercase" | "snake_case",
     {
       "allowArray": false,
       "splitByDots": false
@@ -112,6 +113,32 @@ appTitle: I18N Management System
 app_title: I18N Management System
 ```
 
+:+1: Examples of **correct** code for this rule with `"lowercase"`:
+
+<eslint-code-block language="yaml">
+
+```yaml
+# eslint @intlify/vue-i18n/key-format-style: ['error', 'lowercase']
+
+# ✓ GOOD
+apptitle: I18N Management System
+```
+
+</eslint-code-block>
+
+:-1: Examples of **incorrect** code for this rule with `"lowercase"`:
+
+<eslint-code-block language="yaml">
+
+```yaml
+# eslint @intlify/vue-i18n/key-format-style: ['error', 'lowercase']
+
+# ✗ BAD
+appTitle: I18N Management System
+app_title: I18N Management System
+APP_TITLE: I18N Management System
+```
+
 </eslint-code-block>
 
 :+1: Examples of **correct** code for this rule with `"snake_case"`:
diff --git a/lib/rules/key-format-style.ts b/lib/rules/key-format-style.ts
index 1842a59..30da161 100644
--- a/lib/rules/key-format-style.ts
+++ b/lib/rules/key-format-style.ts
@@ -16,6 +16,7 @@ const debug = debugBuilder('eslint-plugin-vue-i18n:key-format-style')
 const allowedCaseOptions = [
   'camelCase',
   'kebab-case',
+  'lowercase',
   'snake_case',
   'SCREAMING_SNAKE_CASE'
 ] as const
diff --git a/lib/utils/casing.ts b/lib/utils/casing.ts
index 1c98338..1ee99d7 100644
--- a/lib/utils/casing.ts
+++ b/lib/utils/casing.ts
@@ -75,6 +75,20 @@ export function isCamelCase(str: string): boolean {
   return true
 }
 
+/**
+ * Checks whether the given string is lowercase.
+ */
+export function isLowerCase(str: string): boolean {
+  if (
+    hasSymbols(str) ||
+    hasUpper(str) ||
+    /-|_|\s/u.exec(str) // kebab or snake or space
+  ) {
+    return false
+  }
+  return true
+}
+
 /**
  * Checks whether the given string is PascalCase.
  */
@@ -104,6 +118,7 @@ const checkersMap = {
   'kebab-case': isKebabCase,
   snake_case: isSnakeCase,
   camelCase: isCamelCase,
+  lowercase: isLowerCase,
   PascalCase: isPascalCase,
   SCREAMING_SNAKE_CASE: isScreamingSnakeCase
 }
@@ -127,6 +142,7 @@ export function pascalCase(str: string): string {
 export const allowedCaseOptions = [
   'camelCase',
   'kebab-case',
+  'lowercase',
   'PascalCase',
   'snake_case',
   'SCREAMING_SNAKE_CASE'


Test patch:
diff --git a/tests/lib/rules/key-format-style.ts b/tests/lib/rules/key-format-style.ts
index 71ac624..9f75841 100644
--- a/tests/lib/rules/key-format-style.ts
+++ b/tests/lib/rules/key-format-style.ts
@@ -129,6 +129,41 @@ tester.run('key-format-style', rule as never, {
       ...options.json.file,
       options: ['kebab-case']
     },
+    {
+      code: `
+      lowercase:
+        foobar: kebab-value
+      `,
+      ...options.yaml.file
+    },
+    {
+      code: `{
+        "lowercase": {
+          "foobar": "kebab-value"
+        }
+      }
+      `,
+      ...options.json.file
+    },
+    {
+      code: `
+      en-US:
+        lowercase:
+          foobar: kebab-value
+      `,
+      ...options.yaml.key
+    },
+    {
+      code: `{
+        "en-US": {
+          "lowercase": {
+            "foobar": "kebab-value"
+          }
+        }
+      }
+      `,
+      ...options.json.key
+    },
     {
       code: `
       snake_case:
@@ -379,6 +414,136 @@ tester.run('key-format-style', rule as never, {
         }
       ]
     },
+    {
+      code: `
+      foo-bar: baz
+      `,
+      ...options.yaml.file,
+      options: ['lowercase'],
+      errors: [
+        {
+          message: '"foo-bar" is not lowercase',
+          line: 2
+        }
+      ]
+    },
+    {
+      code: `
+      en-US:
+        foo-bar: baz
+      `,
+      ...options.yaml.key,
+      options: ['lowercase'],
+      errors: [
+        {
+          message: '"foo-bar" is not lowercase',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      {"foo-bar": "baz"}
+      `,
+      ...options.json.file,
+      options: ['lowercase'],
+      errors: [
+        {
+          message: '"foo-bar" is not lowercase',
+          line: 2
+        }
+      ]
+    },
+    {
+      code: `
+      {"en-US": {
+        "foo-bar": "baz"
+      }}`,
+      ...options.json.key,
+      options: ['lowercase'],
+      errors: [
+        {
+          message: '"foo-bar" is not lowercase',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      kebab-case:
+        snake_case: camelCase
+      `,
+      ...options.yaml.file,
+      options: ['lowercase'],
+      errors: [
+        {
+          message: '"kebab-case" is not lowercase',
+          line: 2
+        },
+        {
+          message: '"snake_case" is not lowercase',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `{
+        "kebab-case": {
+          "snake_case": "camelCase"
+        }
+      }
+      `,
+      ...options.json.file,
+      options: ['lowercase'],
+      errors: [
+        {
+          message: '"kebab-case" is not lowercase',
+          line: 2
+        },
+        {
+          message: '"snake_case" is not lowercase',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      SCREAMING_SNAKE_CASE:
+        PascalCase: camelCase
+      `,
+      ...options.yaml.file,
+      options: ['lowercase'],
+      errors: [
+        {
+          message: '"SCREAMING_SNAKE_CASE" is not lowercase',
+          line: 2
+        },
+        {
+          message: '"PascalCase" is not lowercase',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `{
+        "SCREAMING_SNAKE_CASE": {
+          "PascalCase": "camelCase"
+        }
+      }
+      `,
+      ...options.json.file,
+      options: ['lowercase'],
+      errors: [
+        {
+          message: '"SCREAMING_SNAKE_CASE" is not lowercase',
+          line: 2
+        },
+        {
+          message: '"PascalCase" is not lowercase',
+          line: 3
+        }
+      ]
+    },
     {
       code: `
       - foo