Skip to content
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

Added useEuiI18n hook #3749

Merged
merged 7 commits into from
Jul 16, 2020
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## [`master`](https://github.com/elastic/eui/tree/master)

- Added `useEuiI18n` hook for localization ([#3749](https://github.com/elastic/eui/pull/3749))

**Bug fixes**

- Fixed `EuiComboBox` always showing a scrollbar ([#3744](https://github.com/elastic/eui/pull/3744))
Expand Down
42 changes: 41 additions & 1 deletion scripts/babel/fetch-i18n-strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@ function getCodeForExpression(expressionNode) {
])).code;
}

function handleHookPath(path) {
const symbols = [];

const arguments = path.node.arguments;

if (arguments[0].type !== 'StringLiteral') return symbols;

const token = arguments[0].value;
const defStringNode = arguments[1];
let defString;
let highlighting;

if (defStringNode.type === 'StringLiteral') {
defString = defStringNode.value;
highlighting = 'string';
} else if (defStringNode.type === 'ArrowFunctionExpression') {
defString = getCodeForExpression(defStringNode);
highlighting = 'code';
}

symbols.push({
token,
defString,
highlighting,
loc: path.node.loc,
});

return symbols;
}

function handleJSXPath(path) {
const symbols = [];

Expand Down Expand Up @@ -76,7 +106,17 @@ function traverseFile(filepath) {
);
}
}
}
},
CallExpression(path) {
if (path.node.callee && path.node.callee.type === 'Identifier' && path.node.callee.name === 'useEuiI18n') {
const symbols = handleHookPath(path);
for (let i = 0; i < symbols.length; i++) {
tokenMappings.push(
{ ...symbols[i], filepath: relative(rootDir, filepath) }
);
}
}
},
}
);
}
Expand Down
198 changes: 194 additions & 4 deletions scripts/eslint-plugin/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ function attributesArrayToLookup(attributesArray) {
}

function getDefinedValues(valuesNode) {
if (valuesNode == null || valuesNode.expression.properties == null) return new Set();
return valuesNode.expression.properties.reduce(
if (valuesNode == null || valuesNode.properties == null) return new Set();
return valuesNode.properties.reduce(
(valueNames, property) => {
valueNames.add(property.key.name);
return valueNames;
Expand Down Expand Up @@ -223,7 +223,9 @@ module.exports = {
}

// validate default string interpolation matches values
const valueNames = getDefinedValues(attributes.values);
const valueNames = getDefinedValues(
attributes.values && attributes.values.expression
);

if (attributes.default.type === 'Literal') {
// default is a string literal
Expand Down Expand Up @@ -319,7 +321,195 @@ module.exports = {
}

// debugger;
}
},
CallExpression(node) {
// Only process calls to useEuiI18n
if (
!node.callee ||
node.callee.type !== 'Identifier' ||
node.callee.name !== 'useEuiI18n'
)
return;

const arguments = node.arguments;

const isSingleToken = arguments[0].type === 'Literal';

// validate argument types
if (isSingleToken) {
// default must be either a Literal of an ArrowFunctionExpression
const defaultArg = arguments[1];
const isLiteral = defaultArg.type === 'Literal';
const isArrowExpression =
defaultArg.type === 'ArrowFunctionExpression';
if (!isLiteral && !isArrowExpression) {
context.report({
node,
loc: defaultArg.loc,
messageId: 'invalidDefaultType',
data: { type: defaultArg.type },
});
return;
}
} else {
const tokensArg = arguments[0];
const defaultsArg = arguments[1];

// tokens must be an array of Literals
if (tokensArg.type !== 'ArrayExpression') {
context.report({
node,
loc: tokensArg.loc,
messageId: 'invalidTokensType',
data: { type: tokensArg.type },
});
return;
}

for (let i = 0; i < tokensArg.elements.length; i++) {
const tokenNode = tokensArg.elements[i];
if (
tokenNode.type !== 'Literal' ||
typeof tokenNode.value !== 'string'
) {
context.report({
node,
loc: tokenNode.loc,
messageId: 'invalidTokensType',
data: { type: tokenNode.type }
});
return;
}
}

// defaults must be an array of either Literals or ArrowFunctionExpressions
if (defaultsArg.type !== 'ArrayExpression') {
context.report({
node,
loc: defaultsArg.loc,
messageId: 'invalidDefaultsType',
data: { type: defaultsArg.type }
});
return;
}

for (let i = 0; i < defaultsArg.elements.length; i++) {
const defaultNode = defaultsArg.elements[i];
if (
defaultNode.type !== 'Literal' ||
typeof defaultNode.value !== 'string'
) {
context.report({
node,
loc: defaultNode.loc,
messageId: 'invalidDefaultsType',
data: { type: defaultNode.type }
});
return;
}
}
}

if (isSingleToken) {
const tokenArgument = arguments[0];
const defaultArgument = arguments[1];
const valuesArgument = arguments[2];

// validate token format
const tokenParts = tokenArgument.value.split('.');
if (
tokenParts.length <= 1 ||
tokenParts[0] !== expectedTokenNamespace
) {
context.report({
node,
loc: tokenArgument.loc,
messageId: 'invalidToken',
data: {
tokenValue: tokenArgument.value,
tokenNamespace: expectedTokenNamespace,
},
});
}

// validate default string interpolation matches values
const valueNames = getDefinedValues(valuesArgument);

if (defaultArgument.type === 'Literal') {
// default is a string literal
const expectedNames = getExpectedValueNames(defaultArgument.value);
if (areSetsEqual(expectedNames, valueNames) === false) {
context.report({
node,
loc: valuesArgument.loc,
messageId: 'mismatchedValues',
data: {
expected: formatSet(expectedNames),
provided: formatSet(valueNames),
},
});
}
} else {
// default is a function
// validate the destructured param defined by default function match the values
const defaultFn = defaultArgument;
const objProperties =
defaultFn.params && defaultFn.params[0]
? defaultFn.params[0].properties
: [];
const expectedNames = new Set(
objProperties.map(property => property.key.name)
);
if (areSetsEqual(valueNames, expectedNames) === false) {
context.report({
node,
loc: valuesArgument.loc,
messageId: 'mismatchedValues',
data: {
expected: formatSet(expectedNames),
provided: formatSet(valueNames),
},
});
}
}
} else {
// has multiple tokens
const tokensArgument = arguments[0];
const defaultsArgument = arguments[1];

// validate their names
const tokens = tokensArgument.elements;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const tokenParts = token.value.split('.');
if (
tokenParts.length <= 1 ||
tokenParts[0] !== expectedTokenNamespace
) {
context.report({
node,
loc: token.loc,
messageId: 'invalidToken',
data: { tokenValue: token.value, tokenNamespace: expectedTokenNamespace }
});
}
}

// validate the number of tokens equals the number of defaults
const defaults = defaultsArgument.elements;
if (tokens.length !== defaults.length) {
context.report({
node,
loc: node.loc,
messageId: 'mismatchedTokensAndDefaults',
data: {
tokenLength: tokens.length,
defaultsLength: defaults.length,
},
});
}
}
},
// callback functions
};
}
Expand Down
Loading