Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
Evil Sharkey
Manage
Activity
Members
Labels
Plan
Issues
0
Issue boards
Milestones
Wiki
Requirements
Code
Merge requests
0
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package Registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
This is an archived project. Repository and other project resources are read-only.
Show more breadcrumbs
23Sonics
Evil Sharkey
Commits
82674d87
Commit
82674d87
authored
5 months ago
by
dakkar
Browse files
Options
Downloads
Patches
Plain Diff
lint all uses of translations
parent
42e2a586
No related branches found
Branches containing commit
No related tags found
No related merge requests found
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
eslint/locale.js
+145
-0
145 additions, 0 deletions
eslint/locale.js
eslint/locale.test.js
+29
-0
29 additions, 0 deletions
eslint/locale.test.js
packages/frontend/eslint.config.js
+5
-0
5 additions, 0 deletions
packages/frontend/eslint.config.js
with
179 additions
and
0 deletions
eslint/locale.js
0 → 100644
+
145
−
0
View file @
82674d87
/* This is a ESLint rule to report use of the `i18n.ts` and `i18n.tsx`
* objects that reference translation items that don't actually exist
* in the lexicon (the `locale/` files)
*/
/* given a MemberExpression node, collects all the member names
*
* e.g. for a bit of code like `foo=one.two.three`, `collectMembers`
* called on the node for `three` would return `['one', 'two',
* 'three']`
*/
function
collectMembers
(
node
)
{
if
(
!
node
)
return
[];
if
(
node
.
type
!==
'
MemberExpression
'
)
return
[];
return
[
node
.
property
.
name
,
...
collectMembers
(
node
.
parent
)
];
}
/* given an object and an array of names, recursively descends the
* object via those names
*
* e.g. `walkDown({one:{two:{three:15}}},['one','two','three'])` would
* return 15
*/
function
walkDown
(
locale
,
path
)
{
if
(
!
locale
)
return
null
;
if
(
!
path
||
path
.
length
===
0
)
return
locale
;
return
walkDown
(
locale
[
path
[
0
]],
path
.
slice
(
1
));
}
/* given a MemberExpression node, returns its attached CallExpression
* node if present
*
* e.g. for a bit of code like `foo=one.two.three()`,
* `findCallExpression` called on the node for `three` would return
* the node for function call (which is the parent of the `one` and
* `two` nodes, and holds the nodes for the argument list)
*
* if the code had been `foo=one.two.three`, `findCallExpression`
* would have returned null, because there's no function call attached
* to the MemberExpressions
*/
function
findCallExpression
(
node
)
{
if
(
node
.
type
===
'
CallExpression
'
)
return
node
if
(
node
.
parent
?.
type
===
'
CallExpression
'
)
return
node
.
parent
;
if
(
node
.
parent
?.
type
===
'
MemberExpression
'
)
return
findCallExpression
(
node
.
parent
);
return
null
;
}
/* the actual rule body
*/
function
theRule
(
context
)
{
// we get the locale/translations via the options; it's the data
// that goes into a specific language's JSON file, see
// `scripts/build-assets.mjs`
const
locale
=
context
.
options
[
0
];
return
{
// for all object member access that have an identifier 'i18n'...
'
MemberExpression:has(> Identifier[name=i18n])
'
:
(
node
)
=>
{
// sometimes we get MemberExpression nodes that have a
// *descendent* with the right identifier: skip them, we'll get
// the right ones as well
if
(
node
.
object
?.
name
!=
'
i18n
'
)
{
return
;
}
// `method` is going to be `'ts'` or `'tsx'`, `path` is going to
// be the various translation steps/names
const
[
method
,
...
path
]
=
collectMembers
(
node
);
const
pathStr
=
`i18n.
${
method
}
.
${
path
.
join
(
'
.
'
)}
`
;
// does that path point to a real translation?
const
matchingNode
=
walkDown
(
locale
,
path
);
if
(
!
matchingNode
)
{
context
.
report
({
node
,
message
:
`translation missing for
${
pathStr
}
`
,
});
return
;
}
// some more checks on how the translation is called
if
(
method
==
'
ts
'
)
{
if
(
matchingNode
.
match
(
/
\{
/
))
{
context
.
report
({
node
,
message
:
`translation for
${
pathStr
}
is parametric, but called via 'ts'`
,
});
return
;
}
if
(
findCallExpression
(
node
))
{
context
.
report
({
node
,
message
:
`translation for
${
pathStr
}
is not parametric, but is called as a function`
,
});
}
}
if
(
method
==
'
tsx
'
)
{
if
(
!
matchingNode
.
match
(
/
\{
/
))
{
context
.
report
({
node
,
message
:
`translation for
${
pathStr
}
is not parametric, but called via 'tsx'`
,
});
return
;
}
const
callExpression
=
findCallExpression
(
node
);
if
(
!
callExpression
)
{
context
.
report
({
node
,
message
:
`translation for
${
pathStr
}
is parametric, but not called as a function`
,
});
return
;
}
const
parameterCount
=
[...
matchingNode
.
matchAll
(
/
\{
/g
)].
length
??
0
;
const
argumentCount
=
callExpression
.
arguments
.
length
;
if
(
parameterCount
!==
argumentCount
)
{
context
.
report
({
node
,
message
:
`translation for
${
pathStr
}
has
${
parameterCount
}
parameters, but is called with
${
argumentCount
}
arguments`
,
});
return
;
}
}
},
};
}
module
.
exports
=
{
meta
:
{
type
:
'
problem
'
,
docs
:
{
description
:
'
assert that all translations used are present in the locale files
'
,
},
schema
:
[
// here we declare that we need the locale/translation as a
// generic object
{
type
:
'
object
'
,
additionalProperties
:
true
},
],
},
create
:
theRule
,
};
This diff is collapsed.
Click to expand it.
eslint/locale.test.js
0 → 100644
+
29
−
0
View file @
82674d87
const
{
RuleTester
}
=
require
(
"
eslint
"
);
const
localeRule
=
require
(
"
./locale
"
);
const
locale
=
{
foo
:
{
bar
:
'
ok
'
,
baz
:
'
good {x}
'
},
top
:
'
123
'
};
const
ruleTester
=
new
RuleTester
();
ruleTester
.
run
(
'
sharkey-locale
'
,
localeRule
,
{
valid
:
[
{
code
:
'
i18n.ts.foo.bar
'
,
options
:
[
locale
]
},
{
code
:
'
i18n.ts.top
'
,
options
:
[
locale
]
},
{
code
:
'
i18n.tsx.foo.baz(1)
'
,
options
:
[
locale
]
},
{
code
:
'
whatever.i18n.ts.blah.blah
'
,
options
:
[
locale
]
},
{
code
:
'
whatever.i18n.tsx.does.not.matter
'
,
options
:
[
locale
]
},
],
invalid
:
[
{
code
:
'
i18n.ts.not
'
,
options
:
[
locale
],
errors
:
1
},
{
code
:
'
i18n.tsx.deep.not
'
,
options
:
[
locale
],
errors
:
1
},
{
code
:
'
i18n.tsx.deep.not(12)
'
,
options
:
[
locale
],
errors
:
1
},
{
code
:
'
i18n.tsx.top(1)
'
,
options
:
[
locale
],
errors
:
1
},
{
code
:
'
i18n.ts.foo.baz
'
,
options
:
[
locale
],
errors
:
1
},
{
code
:
'
i18n.tsx.foo.baz
'
,
options
:
[
locale
],
errors
:
1
},
],
},
);
This diff is collapsed.
Click to expand it.
packages/frontend/eslint.config.js
+
5
−
0
View file @
82674d87
...
...
@@ -4,6 +4,8 @@ import parser from 'vue-eslint-parser';
import
pluginVue
from
'
eslint-plugin-vue
'
;
import
pluginMisskey
from
'
@misskey-dev/eslint-plugin
'
;
import
sharedConfig
from
'
../shared/eslint.config.js
'
;
import
localeRule
from
'
../../eslint/locale.js
'
;
import
{
build
as
buildLocales
}
from
'
../../locales/index.js
'
;
export
default
[
...
sharedConfig
,
...
...
@@ -14,6 +16,7 @@ export default [
...
pluginVue
.
configs
[
'
flat/recommended
'
],
{
files
:
[
'
{src,test,js,@types}/**/*.{ts,vue}
'
],
plugins
:
{
sharkey
:
{
rules
:
{
locale
:
localeRule
}
}
},
languageOptions
:
{
globals
:
{
...
Object
.
fromEntries
(
Object
.
entries
(
globals
.
node
).
map
(([
key
])
=>
[
key
,
'
off
'
])),
...
...
@@ -44,6 +47,8 @@ export default [
},
},
rules
:
{
'
sharkey/locale
'
:
[
'
error
'
,
buildLocales
()[
'
ja-JP
'
]],
'
@typescript-eslint/no-empty-interface
'
:
[
'
error
'
,
{
allowSingleExtends
:
true
,
}],
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment